Compare commits

...

38 Commits

Author SHA1 Message Date
Sercan Yemen
5f2372cc08 Updated Angular Calendar due to broken AoT 2018-01-02 12:29:32 +03:00
Sercan Yemen
6f315aa38e Small fixes to Auth pages 2018-01-02 12:00:46 +03:00
Sercan Yemen
0653b5f36b Academy app small tweaks and responsive fixes 2017-12-28 14:00:06 +03:00
Sercan Yemen
f13120bc01 Updated Academy example data 2017-12-28 11:53:02 +03:00
Sercan Yemen
a6c56518bc New: Academy (Courses) app 2017-12-28 10:49:29 +03:00
Sercan Yemen
ecae48f3d0 Updated Angular and Angular Material + various other components
+ Increased the Fuse version number
2017-12-28 10:49:04 +03:00
Sercan Yemen
c74751e0f4 Added new SlideIn Animation
+ Improvements on perfect scrollbar
2017-12-28 10:48:05 +03:00
Sercan Yemen
2b755fa669 Replaced depreciated Quick panel focus helpers 2017-12-28 10:47:19 +03:00
Sercan Yemen
ca0f46b414 Fix: Scroll doesn't propagate by default (PerfectScrollbar) 2017-12-26 10:48:58 +03:00
Sercan Yemen
528fa31df6 Fixed: (Firefox) Navbar logo doesn't show when navbar collapsed 2017-12-21 10:04:56 +03:00
Sercan Yemen
abfb2a6706 Fixed: Mail Compose dialog responsive issues
+ Added Show/Hide CC & BCC fields toggle button
2017-12-19 12:21:22 +03:00
Sercan Yemen
b1ab11393e Fix: (Ecommerce) Orders and Products tables not scrolling when custom scrollbars disabled 2017-12-19 10:27:33 +03:00
Sercan Yemen
786883eb10 Updated season images
+ Fixed e-commerce app images
+ Stop animations in mail-ngrx app
2017-12-18 12:24:47 +03:00
Sercan Yemen
e477f797d0 Updated Material Moment Adapter package 2017-12-14 16:06:07 +03:00
Sercan Yemen
fb196c3864 Angular & Angular Material versions updated 2017-12-14 15:51:09 +03:00
mustafahlvc
5cf44962fc (Mail) back arrow visibility updated for responsive 2017-12-14 12:27:08 +03:00
mustafahlvc
06b0c3775a (Mail-ngrx) missing deSelectCurrentFunction() added to mail.component,
back arrow visibility updated for responsive
2017-12-14 12:25:48 +03:00
mustafahlvc
26690990f0 (Mail-ngrx) currentMail fixes on html 2017-12-14 11:52:53 +03:00
mustafahlvc
377092d9ec (Mail-ngrx) unread, selected class added for mail list item 2017-12-14 11:10:35 +03:00
mustafahlvc
abede386c8 (Mail-ngrx) Ngrx version of Mail App added. 2017-12-12 22:31:50 +03:00
Sercan Yemen
242feaa169 Updated Angular to 5.0.1
+ Updated Angular Material to 5.0.0 stable
+ Install the ngx-dnd package from its original location
+ Updated various other packages
+ Increased the Fuse version
2017-12-08 10:34:39 +03:00
Sercan Yemen
7c2494a82c Fixed: Shortcut items duplicates on search in some cases 2017-12-08 10:31:39 +03:00
Sercan Yemen
5c2e717a40 Make sure the nav item has children before trying to get them
+ Added LICENSE file
+ Renamed the KnowledgeBase demo module
2017-12-08 09:34:30 +03:00
Sercan Yemen
6ae0a9760d Fix the missing backgrounds
+ Increased the shadow weight of the content on Auth, Coming Soon and Maintenance pages
2017-12-06 14:39:24 +03:00
Sercan Yemen
2a10f3e443 Moved the navigation.model.ts into its own folder
+ Added support for translations in nav items
+ Added badge support for collapsable nav items
+ Initialize the navigation from app.component rather then navigation.service
2017-12-06 14:10:48 +03:00
Sercan Yemen
db7a00440c Re-activated mail search
+ Small formatting in mail translation
2017-12-06 14:09:31 +03:00
Sercan Yemen
0e1c589399 Use absolute paths for importing fuse.scss 2017-12-06 11:40:43 +03:00
Sercan Yemen
2f419b1af5 Removed unused HttpModule import 2017-12-06 11:40:16 +03:00
Sercan Yemen
effd3cefcb Scrumboard and Contacts app dialogs responsive fixes 2017-12-06 11:35:58 +03:00
Sercan Yemen
21fd488a8e ngx-color-picker style adjustments
+ Calendar event form responsive fixes
2017-12-06 11:26:57 +03:00
Sercan Yemen
37a5c69269 Removed Google Calendar images as they are no longer available freely from Google 2017-12-06 10:34:40 +03:00
Sercan Yemen
9f440b1bf2 Some code inspection fixes 2017-11-30 15:55:00 +03:00
Sercan Yemen
a65f61cce4 Form Stepper examples
+ Simplified the mat-select style
+ Further simplified the mat-select in dashboard widgets
2017-11-30 15:37:32 +03:00
Sercan Yemen
0d8fe0be72 Trigger expand/collapse of the navigation on ngOnInit to update the active item 2017-11-30 10:37:35 +03:00
Sercan Yemen
2bbc90af64 Perfect scrollbar shouldn't be destroyed if it's not available
+ Print media css classes updated to hide the perfect scrollbars
2017-11-30 10:22:18 +03:00
Sercan Yemen
ad21d9fed5 Further improvements to Auth pages 2017-11-30 10:21:22 +03:00
Sercan Yemen
4c6ef29e20 Fixed: Behavior Subject must be array 2017-11-28 10:48:52 +03:00
Sercan Yemen
a74c5108fd Fixed: Couple small issues with Auth forms
Added custom validator for Password Matching to Register forms
2017-11-28 10:42:41 +03:00
183 changed files with 7024 additions and 1181 deletions

1
LICENSE Normal file
View File

@@ -0,0 +1 @@
https://themeforest.net/licenses/terms/regular

1097
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "fuse2",
"version": "1.2.3",
"license": "",
"version": "1.3.1",
"license": "https://themeforest.net/licenses/terms/regular",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -16,56 +16,61 @@
"private": true,
"dependencies": {
"@agm/core": "1.0.0-beta.2",
"@angular/animations": "5.0.3",
"@angular/cdk": "5.0.0-rc.1",
"@angular/common": "5.0.3",
"@angular/compiler": "5.0.3",
"@angular/core": "5.0.3",
"@angular/flex-layout": "2.0.0-beta.10-4905443",
"@angular/forms": "5.0.3",
"@angular/http": "5.0.3",
"@angular/material": "5.0.0-rc.1",
"@angular/material-moment-adapter": "5.0.0-rc.1",
"@angular/platform-browser": "5.0.3",
"@angular/platform-browser-dynamic": "5.0.3",
"@angular/router": "5.0.3",
"@ngx-translate/core": "8.0.0",
"@angular/animations": "5.1.2",
"@angular/cdk": "5.0.2",
"@angular/common": "5.1.2",
"@angular/compiler": "5.1.2",
"@angular/core": "5.1.2",
"@angular/flex-layout": "2.0.0-beta.12",
"@angular/forms": "5.1.2",
"@angular/http": "5.1.2",
"@angular/material": "5.0.2",
"@angular/material-moment-adapter": "5.0.2",
"@angular/platform-browser": "5.1.2",
"@angular/platform-browser-dynamic": "5.1.2",
"@angular/router": "5.1.2",
"@ngx-translate/core": "9.0.2",
"@swimlane/ngx-charts": "7.0.1",
"@swimlane/ngx-datatable": "11.1.4",
"@withinpixels/ngx-dnd": "3.1.0",
"angular-calendar": "0.22.1",
"angular-in-memory-web-api": "0.5.1",
"@swimlane/ngx-datatable": "11.1.7",
"@swimlane/ngx-dnd": "3.1.0",
"angular-calendar": "0.23.0",
"angular-in-memory-web-api": "0.5.2",
"angular2-markdown": "1.6.0",
"classlist.js": "1.1.20150312",
"core-js": "2.5.1",
"core-js": "2.5.3",
"d3": "4.12.0",
"hammerjs": "2.0.8",
"highlight.js": "9.12.0",
"intl": "1.2.5",
"moment": "2.19.2",
"ngx-color-picker": "5.0.4",
"moment": "2.20.1",
"@ngrx/effects": "4.1.1",
"@ngrx/router-store": "4.1.1",
"@ngrx/store": "4.1.1",
"@ngrx/store-devtools": "4.1.1",
"ngrx-store-freeze": "0.2.0",
"ngx-color-picker": "5.3.0",
"ngx-cookie-service": "1.0.9",
"perfect-scrollbar": "1.2.0",
"rxjs": "5.5.2",
"perfect-scrollbar": "1.3.0",
"rxjs": "5.5.6",
"web-animations-js": "2.3.1",
"zone.js": "0.8.18"
},
"devDependencies": {
"@angular/cli": "1.5.4",
"@angular/compiler-cli": "5.0.3",
"@angular/language-service": "5.0.3",
"@angular/cli": "1.6.2",
"@angular/compiler-cli": "5.1.2",
"@angular/language-service": "5.1.2",
"@angularclass/hmr": "2.1.3",
"@types/jasmine": "2.5.54",
"@types/jasminewd2": "2.0.3",
"@types/node": "6.0.92",
"codelyzer": "4.0.1",
"@types/node": "6.0.95",
"codelyzer": "4.0.2",
"jasmine-core": "2.6.4",
"jasmine-spec-reporter": "4.1.1",
"karma": "1.7.1",
"karma-chrome-launcher": "2.1.1",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "1.3.0",
"karma-jasmine": "1.1.0",
"karma-coverage-istanbul-reporter": "1.3.1",
"karma-jasmine": "1.1.1",
"karma-jasmine-html-reporter": "0.2.2",
"protractor": "5.1.2",
"ts-node": "3.2.2",

View File

@@ -1,6 +1,12 @@
import { Component } from '@angular/core';
import { FuseSplashScreenService } from './core/services/splash-screen.service';
import { TranslateService } from '@ngx-translate/core';
import { FuseTranslationLoaderService } from './core/services/translation-loader.service';
import { FuseNavigationService } from './core/components/navigation/navigation.service';
import { FuseNavigationModel } from './navigation/navigation.model';
import { locale as navigationEnglish } from './navigation/i18n/en';
import { locale as navigationTurkish } from './navigation/i18n/tr';
@Component({
selector : 'fuse-root',
@@ -10,8 +16,10 @@ import { TranslateService } from '@ngx-translate/core';
export class AppComponent
{
constructor(
private fuseNavigationService: FuseNavigationService,
private fuseSplashScreen: FuseSplashScreenService,
private translate: TranslateService
private translate: TranslateService,
private translationLoader: FuseTranslationLoaderService
)
{
// Add languages
@@ -22,5 +30,11 @@ export class AppComponent
// Use a language
this.translate.use('en');
// Set the navigation model
this.fuseNavigationService.setNavigationModel(new FuseNavigationModel());
// Set the navigation translations
this.translationLoader.loadTranslations(navigationEnglish, navigationTurkish);
}
}

View File

@@ -1,6 +1,5 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpModule } from '@angular/http';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule, Routes } from '@angular/router';
@@ -22,12 +21,17 @@ import { ServicesModule } from './main/content/services/services.module';
import { FuseAngularMaterialModule } from './main/content/components/angular-material/angular-material.module';
import { MarkdownModule } from 'angular2-markdown';
import { TranslateModule } from '@ngx-translate/core';
import { AppStoreModule } from './store/store.module';
const appRoutes: Routes = [
{
path : 'apps/mail',
loadChildren: './main/content/apps/mail/mail.module#FuseMailModule'
},
{
path : 'apps/mail-ngrx',
loadChildren: './main/content/apps/mail-ngrx/mail.module#FuseMailNgrxModule'
},
{
path : 'apps/chat',
loadChildren: './main/content/apps/chat/chat.module#FuseChatModule'
@@ -40,6 +44,10 @@ const appRoutes: Routes = [
path : 'apps/e-commerce',
loadChildren: './main/content/apps/e-commerce/e-commerce.module#FuseEcommerceModule'
},
{
path : 'apps/academy',
loadChildren: './main/content/apps/academy/academy.module#FuseAcademyModule'
},
{
path : 'apps/todo',
loadChildren: './main/content/apps/todo/todo.module#FuseTodoModule'
@@ -68,23 +76,19 @@ const appRoutes: Routes = [
],
imports : [
BrowserModule,
HttpModule,
HttpClientModule,
BrowserAnimationsModule,
RouterModule.forRoot(appRoutes),
SharedModule,
MarkdownModule.forRoot(),
TranslateModule.forRoot(),
InMemoryWebApiModule.forRoot(FuseFakeDbService, {
delay : 0,
passThruUnknownUrl: true
}),
AppStoreModule,
FuseMainModule,
ProjectModule,
PagesModule,
UIModule,
ServicesModule,

View File

@@ -76,6 +76,53 @@ export const fuseAnimations = [
transition('0 => 1', animate('300ms ease-in'))
]),
trigger('slideIn', [
transition('void => left', [
style({
transform: 'translateX(100%)'
}),
animate('300ms ease-in',
style({
transform: 'translateX(0)'
})
)
]
),
transition('left => void', [
style({
transform: 'translateX(0)'
}),
animate('300ms ease-in',
style({
transform: 'translateX(-100%)'
})
)
]
),
transition('void => right', [
style({
transform: 'translateX(-100%)'
}),
animate('300ms ease-in',
style({
transform: 'translateX(0)'
})
)
]
),
transition('right => void', [
style({
transform: 'translateX(0)'
}),
animate('300ms ease-in',
style({
transform: 'translateX(100%)'
})
)
]
),
]),
trigger('slideInLeft', [
state('void', style({
transform: 'translateX(-100%)',

View File

@@ -1,6 +1,10 @@
<a class="nav-link" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
<mat-icon class="collapse-arrow">keyboard_arrow_right</mat-icon>
</a>

View File

@@ -1,8 +1,8 @@
<a class="nav-link" *ngIf="item.url" [routerLink]="[item.url]" routerLinkActive="active"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
@@ -10,8 +10,8 @@
<span class="nav-link" *ngIf="item.function" (click)="item.function()" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>

View File

@@ -0,0 +1,5 @@
export interface FuseNavigationModelInterface
{
model: any[];
}

View File

@@ -1,6 +1,6 @@
import { EventEmitter, Injectable } from '@angular/core';
import { NavigationModel } from '../../../navigation.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { FuseNavigationModelInterface } from './navigation.model';
@Injectable()
export class FuseNavigationService
@@ -8,13 +8,11 @@ export class FuseNavigationService
onNavCollapseToggle = new EventEmitter<any>();
onNavCollapseToggled = new EventEmitter<any>();
onNavigationModelChange: BehaviorSubject<any> = new BehaviorSubject({});
navigationModel: NavigationModel;
navigationModel: FuseNavigationModelInterface;
flatNavigation: any[] = [];
constructor()
{
this.navigationModel = new NavigationModel();
this.onNavigationModelChange.next(this.navigationModel.model);
}
/**
@@ -155,8 +153,15 @@ export class FuseNavigationService
*/
getFlatNavigation(navigationItems?)
{
// If navigation items not provided,
// that means we are running the function
// for the first time...
if ( !navigationItems )
{
// Reset the flat navigation
this.flatNavigation = [];
// Get the entire navigation model
navigationItems = this.navigationModel.model;
}
@@ -181,7 +186,10 @@ export class FuseNavigationService
if ( navItem.type === 'collapse' || navItem.type === 'group' )
{
this.getFlatNavigation(navItem.children);
if ( navItem.children )
{
this.getFlatNavigation(navItem.children);
}
}
}

View File

@@ -1,8 +1,13 @@
<a class="nav-link" matRipple (click)="toggleOpen($event)">
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
<mat-icon class="collapse-arrow">keyboard_arrow_right</mat-icon>
</a>
<div class="children" [@slideInOut]="isOpen">
<ng-container *ngFor="let item of item.children">
<fuse-nav-vertical-item *ngIf="item.type=='item'" [item]="item"></fuse-nav-vertical-item>

View File

@@ -69,6 +69,20 @@ export class FuseNavVerticalCollapseComponent implements OnInit
);
}
ngOnInit()
{
// Check if the url can be found in
// one of the children of this item
if ( this.isUrlInChildren(this.item, this.router.url) )
{
this.expand();
}
else
{
this.collapse();
}
}
/**
* Toggle collapse
*
@@ -108,6 +122,7 @@ export class FuseNavVerticalCollapseComponent implements OnInit
{
return;
}
this.isOpen = false;
this.navigationService.onNavCollapseToggle.emit();
}
@@ -175,8 +190,4 @@ export class FuseNavVerticalCollapseComponent implements OnInit
return false;
}
ngOnInit()
{
}
}

View File

@@ -1,6 +1,7 @@
<div class="group-title">
<span class="hint-text">{{ item.title }}</span>
<span class="hint-text" [translate]="item.translate">{{ item.title }}</span>
</div>
<div class="group-items">
<ng-container *ngFor="let item of item.children">
<fuse-nav-vertical-group *ngIf="item.type=='group'" [item]="item"></fuse-nav-vertical-group>

View File

@@ -1,8 +1,8 @@
<a class="nav-link" *ngIf="item.url" [routerLink]="[item.url]" routerLinkActive="active"
[routerLinkActiveOptions]="{exact: item.exactMatch || false}" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>
@@ -10,8 +10,8 @@
<span class="nav-link" *ngIf="item.function" (click)="item.function()" matRipple>
<mat-icon class="nav-link-icon" *ngIf="item.icon">{{item.icon}}</mat-icon>
<span class="nav-link-title">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge"
<span class="nav-link-title" [translate]="item.translate">{{item.title}}</span>
<span class="nav-link-badge" *ngIf="item.badge" [translate]="item.badge.translate"
[ngStyle]="{'background-color': item.badge.bg,'color': item.badge.fg}">
{{item.badge.title}}
</span>

View File

@@ -62,4 +62,23 @@ fuse-widget {
transform: rotateY(360deg);
}
}
.mat-form-field {
&.mat-form-field-type-mat-select {
.mat-input-wrapper {
padding: 16px 0;
.mat-input-infix {
border: none;
padding: 0;
}
}
.mat-input-underline {
display: none;
}
}
}
}

View File

@@ -27,7 +27,9 @@ export class FuseWidgetComponent implements OnInit, AfterContentInit
setTimeout(() => {
this.toggleButtons.forEach(flipButton => {
this.renderer.listen(flipButton.el.nativeElement, 'click', () => {
this.renderer.listen(flipButton.el.nativeElement, 'click', (event) => {
event.preventDefault();
event.stopPropagation();
this.toggle();
});
});

View File

@@ -7,7 +7,7 @@ import { Platform } from '@angular/cdk/platform';
@Directive({
selector: '[fusePerfectScrollbar]'
})
export class FusePerfectScrollbarDirective implements OnInit, AfterViewInit, OnDestroy
export class FusePerfectScrollbarDirective implements AfterViewInit, OnDestroy
{
onSettingsChanged: Subscription;
isDisableCustomScrollbars = false;
@@ -16,7 +16,7 @@ export class FusePerfectScrollbarDirective implements OnInit, AfterViewInit, OnD
ps;
constructor(
private element: ElementRef,
public element: ElementRef,
private fuseConfig: FuseConfigService,
private platform: Platform
)
@@ -35,11 +35,6 @@ export class FusePerfectScrollbarDirective implements OnInit, AfterViewInit, OnD
}
}
ngOnInit()
{
}
ngAfterViewInit()
{
if ( this.isMobile || this.isDisableCustomScrollbars )
@@ -49,12 +44,14 @@ export class FusePerfectScrollbarDirective implements OnInit, AfterViewInit, OnD
}
// Initialize the perfect-scrollbar
this.ps = new PerfectScrollbar(this.element.nativeElement);
this.ps = new PerfectScrollbar(this.element.nativeElement, {
wheelPropagation: true
});
}
ngOnDestroy()
{
if ( !this.isInitialized )
if ( !this.isInitialized || !this.ps )
{
return;
}

View File

@@ -5,7 +5,7 @@ import { CommonModule } from '@angular/common';
import { MaterialModule } from './material.module';
import { FlexLayoutModule } from '@angular/flex-layout';
import { ColorPickerModule } from 'ngx-color-picker';
import { NgxDnDModule } from '@withinpixels/ngx-dnd';
import { NgxDnDModule } from '@swimlane/ngx-dnd';
import { NgxDatatableModule } from '@swimlane/ngx-datatable';
import { FuseMatSidenavHelperDirective, FuseMatSidenavTogglerDirective } from '../directives/fuse-mat-sidenav-helper/fuse-mat-sidenav-helper.directive';

View File

@@ -28,3 +28,51 @@
.mat-form-field-underline {
background-color: rgba(0, 0, 0, 0.12);
}
// Fix: "Some idiots using table-cell and inline-table in mat-select"
.mat-form-field {
&.mat-form-field-type-mat-select {
.mat-input-infix {
display: inline-flex;
width: auto;
.mat-select-trigger {
display: inline-flex;
align-items: center;
width: 100%;
.mat-select-value {
display: flex;
max-width: none;
margin-right: 8px;
}
.mat-select-arrow-wrapper {
display: inline-flex;
}
}
}
}
}
// Fix: "Stepper icons are broken due to Fuse's icon helpers"
mat-horizontal-stepper,
mat-vertical-stepper {
mat-step-header {
mat-icon {
height: 16px !important;
width: 16px !important;
min-width: 0 !important;
min-height: 0 !important;
color: rgba(255, 255, 255, 0.87) !important;
}
}
}
mat-vertical-stepper {
padding: 16px 0;
}

View File

@@ -47,14 +47,20 @@
}
.nav-link-badge {
display: flex;
align-items: center;
min-width: 20px;
height: 20px;
line-height: 20px;
padding: 0 7px;
font-size: 11px;
font-weight: 500;
border-radius: 20px;
transition: opacity 0.2s ease-in-out 0.1s;
margin-left: 8px;
+ .collapse-arrow {
margin-left: 8px;
}
}
&:hover {

View File

@@ -42,8 +42,8 @@
fuse-footer,
fuse-quick-panel,
fuse-theme-options,
.ps > .ps__scrollbar-x-rail,
.ps > .ps__scrollbar-y-rail {
.ps > .ps__rail-x,
.ps > .ps__rail-y {
display: none !important;
}

View File

@@ -3,20 +3,21 @@
.color-picker {
height: auto !important;
border: none !important;
@include mat-elevation(4);
.preset-area {
//padding: 4px 15px;
padding: 0 0 12px 12px !important;
padding: 0 0 16px 16px !important;
height: 140px;
overflow-y: auto;
overflow-x: hidden;
> hr {
display: none;
}
.preset-label {
display: none;
}
.preset-color {
@@ -25,7 +26,8 @@
margin: 0 !important;
border: none !important;
border-radius: 0 !important;
&:nth-child(14n+3) {
&:nth-child(14n+1) {
clear: both;
}
}

View File

@@ -34,7 +34,7 @@ export class FuseConfigService
footer : 'mat-fuse-dark-900-bg'
},
customScrollbars: true,
routerAnimation : 'fadeIn' // fadeIn, slideUp, slideDown, slideRight, slideLeft
routerAnimation : 'fadeIn' // fadeIn, slideUp, slideDown, slideRight, slideLeft, none
};
/**

View File

@@ -0,0 +1,773 @@
export class AcademyFakeDb
{
public static courses = [
{
'id' : '15459251a6d6b397565',
'title' : 'Basics of Angular',
'slug' : 'basics-of-angular',
'category': 'web',
'length' : 30,
'updated' : 'Jun 28, 2017'
},
{
'id' : '154588a0864d2881124',
'title' : 'Basics of TypeScript',
'slug' : 'basics-of-typeScript',
'category': 'web',
'length' : 60,
'updated' : 'Nov 01, 2017'
},
{
'id' : '15453ba60d3baa5daaf',
'title' : 'Android N: Quick Settings',
'slug' : 'android-n-quick-settings',
'category': 'android',
'length' : 120,
'updated' : 'Jun 28, 2017'
},
{
'id' : '15453a06c08fb021776',
'title' : 'Keep Sensitive Data Safe and Private',
'slug' : 'keep-sensitive-data-safe-and-private',
'category': 'android',
'length' : 45,
'updated' : 'Jun 28, 2017'
},
{
'id' : '15427f4c1b7f3953234',
'title' : 'Building a gRPC Service with Java',
'slug' : 'building-a-grpc-service-with-java',
'category': 'cloud',
'length' : 30,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1542d75d929a603125',
'title' : 'Build a PWA Using Workbox',
'slug' : 'build-a-pwa-using-workbox',
'category': 'web',
'length' : 120,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1543ee3a5b43e0f9f45',
'title' : 'Build an App for the Google Assistant with Firebase and Dialogflow',
'slug' : 'build-an-app-for-the-google-assistant-with-firebase-and-dialogflow',
'category': 'firebase',
'length' : 30,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1543cc4515df3146112',
'title' : 'Cloud Functions for Firebase',
'slug' : 'cloud-functions-for-firebase',
'category': 'firebase',
'length' : 45,
'updated' : 'Jun 28, 2017'
},
{
'id' : '154398a4770d7aaf9a2',
'title' : 'Manage Your Pivotal Cloud Foundry App\'s Using Apigee Edge',
'slug' : 'manage-your-pivotal-cloud-foundry-apps-using-apigee-Edge',
'category': 'cloud',
'length' : 90,
'updated' : 'Jun 28, 2017'
},
{
'id' : '15438351f87dcd68567',
'title' : 'Building Beautiful UIs with Flutter',
'your' : 'building-beautiful-uis-with-flutter',
'category': 'web',
'length' : 90,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1544e43dcdae6ebf876',
'title' : 'Cloud Firestore',
'slug' : 'cloud-firestore',
'category': 'firebase',
'length' : 90,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1541ca7af66da284177',
'title' : 'Customize Network Topology with Subnetworks',
'slug' : 'customize-network-topology-with-subnetworks',
'category': 'web',
'length' : 45,
'updated' : 'Jun 28, 2017'
},
{
'id' : '154297167e781781745',
'title' : 'Looking at Campaign Finance with BigQuery',
'slug' : 'looking-at-campaign-finance-with-bigquery',
'category': 'cloud',
'length' : 60,
'updated' : 'Jun 28, 2017'
},
{
'id' : '154537435d5b32bf11a',
'title' : 'Firebase Android',
'slug' : 'firebase-android',
'category': 'android',
'length' : 45,
'updated' : 'Jun 28, 2017'
},
{
'id' : '154204e45a59b168453',
'title' : 'Simulating a Thread Network Using OpenThread',
'slug' : 'simulating-a-thread-network-using-openthread',
'category': 'web',
'length' : 45,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1541dd1e05dfc439216',
'title' : 'Your First Progressive Web App',
'slug' : 'your-first-progressive-web-app',
'category': 'web',
'length' : 30,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1532dfc67e704e48515',
'title' : 'Launch Cloud Datalab',
'slug' : 'launch-cloud-datalab',
'category': 'cloud',
'length' : 60,
'updated' : 'Jun 28, 2017'
},
{
'id' : '1542e43dfaae6ebf226',
'title' : 'Personalize Your iOS App with Firebase User Management',
'slug' : 'personalize-your-ios-app-with-firebase-user-management',
'category': 'firebase',
'length' : 90,
'updated' : 'Jun 28, 2017'
}
];
public static categories = [
{
'id' : 0,
'value': 'web',
'label': 'Web'
},
{
'id' : 1,
'value': 'firebase',
'label': 'Firebase'
},
{
'id' : 2,
'value': 'cloud',
'label': 'Cloud'
},
{
'id' : 3,
'value': 'android',
'label': 'Android'
}
];
private static demoSteps = [
{
'title' : 'Introduction',
'content': '<h1>Step 1 - Introduction</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Get the sample code',
'content': '<h1>Step 2 - Get the sample code</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Create a Firebase project and Set up your app',
'content': '<h1>Step 3 - Create a Firebase project and Set up your app</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Install the Firebase Command Line Interface',
'content': '<h1>Step 4 - Install the Firebase Command Line Interface</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Deploy and run the web app',
'content': '<h1>Step 5 - Deploy and run the web app</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'The Functions Directory',
'content': '<h1>Step 6 - The Functions Directory</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Import the Cloud Functions and Firebase Admin modules',
'content': '<h1>Step 7 - Import the Cloud Functions and Firebase Admin modules</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Welcome New Users',
'content': '<h1>Step 8 - Welcome New Users</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Images moderation',
'content': '<h1>Step 9 - Images moderation</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'New Message Notifications',
'content': '<h1>Step 10 - New Message Notifications</h1>' +
'<br>' +
'This is an example step of the course. You can put anything in here from example codes to videos.' +
'<br><br>' +
'To install the CLI you need to have installed <b>npm</b> which typically comes with <b>NodeJS</b>.' +
'To install or upgrade the CLI run the following <b>npm</b> command:' +
'<br><br>' +
'<code>npm -g install @angular/cli</code>' +
'<br><br>' +
'To verify that the CLI has been installed correctly, open a console and run:' +
'<br><br>' +
'<code>ng version</code>' +
'<br><br>' +
'<h2>Install dependencies</h2>' +
'<br>' +
'To moderate the images we\'ll need a few Node.js packages:' +
'<br><br>' +
'<ul>' +
'<li>' +
'The Google Cloud Vision Client Library for Node.js: @google-cloud/vision to run the image through the Cloud Vision API to detect inappropriate images.' +
'</li>' +
'<br>' +
'<li>' +
'The Google Cloud Storage Client Library for Node.js: @google-cloud/storage to download and upload the images from Cloud Storage.' +
'</li>' +
'<br>' +
'<li>' +
'A Node.js library allowing us to run processes: child-process-promise to run ImageMagick since the ImageMagick command-line tool comes pre-installed on all Functions instances.' +
'</li>' +
'</ul>' +
'<br>' +
'To install these three packages into your Cloud Functions app, run the following npm install --save command. Make sure that you do this from the functions directory.' +
'<br><br>' +
'<code>npm install --save @google-cloud/vision @google-cloud/storage child-process-promise</code>' +
'<br><br>' +
'This will install the three packages locally and add them as declared dependencies in your package.js file.'
},
{
'title' : 'Congratulations!',
'content': '<h1>Step 11 - Congratulations!</h1>' +
'<br>' +
'You\'ve built a full-fidelity, offline-capable progressive web app by leveraging the power of reusable Web Components and Firebase. Why bother with a native app when you know how to do all that?!'
}
];
public static course = [
{
'id' : '15459251a6d6b397565',
'title' : 'Basics of Angular',
'slug' : 'basics-of-angular',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 30,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '154588a0864d2881124',
'title' : 'Basics of TypeScript',
'slug' : 'basics-of-typeScript',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 60,
'totalSteps' : 11,
'updated' : 'Nov 01, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '15453ba60d3baa5daaf',
'title' : 'Android N: Quick Settings',
'slug' : 'android-n-quick-settings',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'android',
'length' : 120,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '15453a06c08fb021776',
'title' : 'Keep Sensitive Data Safe and Private',
'slug' : 'keep-sensitive-data-safe-and-private',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'android',
'length' : 45,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '15427f4c1b7f3953234',
'title' : 'Building a gRPC Service with Java',
'slug' : 'building-a-grpc-service-with-java',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'cloud',
'length' : 30,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1542d75d929a603125',
'title' : 'Build a PWA Using Workbox',
'slug' : 'build-a-pwa-using-workbox',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 120,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1543ee3a5b43e0f9f45',
'title' : 'Build an App for the Google Assistant with Firebase and Dialogflow',
'slug' : 'build-an-app-for-the-google-assistant-with-firebase-and-dialogflow',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'firebase',
'length' : 30,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1543cc4515df3146112',
'title' : 'Cloud Functions for Firebase',
'slug' : 'cloud-functions-for-firebase',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'firebase',
'length' : 45,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '154398a4770d7aaf9a2',
'title' : 'Manage Your Pivotal Cloud Foundry App\'s Using Apigee Edge',
'slug' : 'manage-your-pivotal-cloud-foundry-apps-using-apigee-Edge',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'cloud',
'length' : 90,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '15438351f87dcd68567',
'title' : 'Building Beautiful UIs with Flutter',
'your' : 'building-beautiful-uis-with-flutter',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 90,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1544e43dcdae6ebf876',
'title' : 'Cloud Firestore',
'slug' : 'cloud-firestore',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'firebase',
'length' : 90,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1541ca7af66da284177',
'title' : 'Customize Network Topology with Subnetworks',
'slug' : 'customize-network-topology-with-subnetworks',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 45,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '154297167e781781745',
'title' : 'Looking at Campaign Finance with BigQuery',
'slug' : 'looking-at-campaign-finance-with-bigquery',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'cloud',
'length' : 60,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '154537435d5b32bf11a',
'title' : 'Firebase Android',
'slug' : 'firebase-android',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'android',
'length' : 45,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '154204e45a59b168453',
'title' : 'Simulating a Thread Network Using OpenThread',
'slug' : 'simulating-a-thread-network-using-openthread',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 45,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1541dd1e05dfc439216',
'title' : 'Your First Progressive Web App',
'slug' : 'your-first-progressive-web-app',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'web',
'length' : 30,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1532dfc67e704e48515',
'title' : 'Launch Cloud Datalab',
'slug' : 'launch-cloud-datalab',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'cloud',
'length' : 60,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
},
{
'id' : '1542e43dfaae6ebf226',
'title' : 'Personalize Your iOS App with Firebase User Management',
'slug' : 'personalize-your-ios-app-with-firebase-user-management',
'description': 'Commits that need to be pushed lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'category' : 'firebase',
'length' : 90,
'totalSteps' : 11,
'updated' : 'Jun 28, 2017',
'steps' : AcademyFakeDb.demoSteps
}
];
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@ import { ScrumboardFakeDb } from './scrumboard';
import { FaqFakeDb } from './faq';
import { KnowledgeBaseFakeDb } from './knowledge-base';
import { ECommerceFakeDb } from './e-commerce';
import { AcademyFakeDb } from './academy';
export class FuseFakeDbService implements InMemoryDbService
{
@@ -52,7 +53,10 @@ export class FuseFakeDbService implements InMemoryDbService
'knowledge-base' : KnowledgeBaseFakeDb.data,
'e-commerce-dashboard' : ECommerceFakeDb.dashboard,
'e-commerce-products' : ECommerceFakeDb.products,
'e-commerce-orders' : ECommerceFakeDb.orders
'e-commerce-orders' : ECommerceFakeDb.orders,
'academy-categories' : AcademyFakeDb.categories,
'academy-courses' : AcademyFakeDb.courses,
'academy-course' : AcademyFakeDb.course
};
}
}

View File

@@ -0,0 +1,47 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../../../../core/modules/shared.module';
import { RouterModule } from '@angular/router';
import { FuseAcademyCoursesComponent } from './courses/courses.component';
import { FuseAcademyCourseComponent } from './course/course.component';
import { AcademyCoursesService } from './courses.service';
import { AcademyCourseService } from './course.service';
const routes = [
{
path : 'courses',
component: FuseAcademyCoursesComponent,
resolve : {
academy: AcademyCoursesService
}
},
{
path : 'courses/:courseId/:courseSlug',
component: FuseAcademyCourseComponent,
resolve : {
academy: AcademyCourseService
}
},
{
path : '**',
redirectTo: 'courses'
}
];
@NgModule({
imports : [
SharedModule,
RouterModule.forChild(routes)
],
declarations: [
FuseAcademyCoursesComponent,
FuseAcademyCourseComponent
],
providers : [
AcademyCoursesService,
AcademyCourseService
]
})
export class FuseAcademyModule
{
}

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class AcademyCourseService implements Resolve<any>
{
onCourseChanged: BehaviorSubject<any> = new BehaviorSubject({});
constructor(private http: HttpClient)
{
}
/**
* The Academy App Main Resolver
*
* @param {ActivatedRouteSnapshot} route
* @param {RouterStateSnapshot} state
* @returns {Observable<any> | Promise<any> | any}
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> | Promise<any> | any
{
return new Promise((resolve, reject) => {
Promise.all([
this.getCourse(route.params.courseId, route.params.courseSlug)
]).then(
() => {
resolve();
},
reject
);
});
}
getCourse(courseId, courseSlug): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/academy-course/' + courseId + '/' + courseSlug)
.subscribe((response: any) => {
this.onCourseChanged.next(response);
resolve(response);
}, reject);
});
}
}

View File

@@ -0,0 +1,100 @@
<div id="academy-course" class="page-layout simple left-sidenav">
<mat-sidenav-container>
<!-- SIDENAV -->
<mat-sidenav class="sidenav" align="start" opened="true" mode="side"
fuseMatSidenavHelper="academy-left-sidenav" mat-is-locked-open="gt-md">
<div class="sidenav-content" fusePerfectScrollbar>
<div class="steps">
<div class="step"
*ngFor="let step of course.steps; let i = index; let last = last; let first = first"
(click)="gotoStep(i)"
[ngClass]="{'current': currentStep === i, 'completed': currentStep > i, 'last': last, 'first': first}">
<div class="index">
<span>{{i + 1}}</span>
</div>
<div class="title">{{step.title}}</div>
</div>
</div>
</div>
</mat-sidenav>
<!-- / SIDENAV -->
<!-- CENTER -->
<div class="center">
<!-- HEADER -->
<div class="header mat-accent-bg p-24" fxLayout="row" fxLayoutAlign="start center">
<button mat-button class="mat-icon-button mr-16 sidenav-toggle"
fuseMatSidenavToggler="academy-left-sidenav" fxHide.gt-md>
<mat-icon>menu</mat-icon>
</button>
<button mat-button class="mat-icon-button mr-16" [routerLink]="'/apps/academy/courses'">
<mat-icon>arrow_back</mat-icon>
</button>
<div>
<h2>{{course.title}}</h2>
</div>
</div>
<!-- / HEADER -->
<!-- CONTENT -->
<div id="course-content" class="content">
<ng-container *ngFor="let step of course.steps; let i = index;">
<div class="course-step" fusePerfectScrollbar
*ngIf="currentStep === i"
[@slideIn]="animationDirection">
<div id="course-step-content" class="course-step-content" [innerHTML]="step.content"></div>
</div>
</ng-container>
</div>
<!-- / CONTENT -->
<div class="step-navigation">
<button mat-fab class="previous mat-accent white-fg"
(click)="gotoPreviousStep()"
[disabled]="currentStep === 0"
[fxHide]="currentStep === 0">
<mat-icon>chevron_left</mat-icon>
</button>
<button mat-fab class="next mat-accent white-fg"
(click)="gotoNextStep()"
[disabled]="currentStep === course.totalSteps - 1"
[fxHide]="currentStep === course.totalSteps - 1">
<mat-icon>chevron_right</mat-icon>
</button>
<button mat-fab class="done mat-green-600-bg"
routerLink="/apps/academy/courses"
[disabled]="currentStep !== course.totalSteps - 1"
[fxShow]="currentStep === course.totalSteps - 1">
<mat-icon>check</mat-icon>
</button>
</div>
</div>
<!-- / CENTER -->
</mat-sidenav-container>
</div>

View File

@@ -0,0 +1,202 @@
@import "src/app/core/scss/fuse";
#academy-course {
.mat-drawer-container {
flex: 1 !important;
> .mat-drawer-content {
flex: 1 !important;
@include media-breakpoint-up('lg') {
z-index: 52;
}
}
}
.sidenav {
.steps {
padding: 16px 0;
.step {
position: relative;
display: flex;
justify-content: flex-start;
align-items: center;
padding: 16px;
cursor: pointer;
color: rgba(0, 0, 0, 0.54);
&.current {
background: mat-color($mat-blue, 50);
color: rgba(0, 0, 0, 0.87);
.index {
span {
border: 2px solid mat-color($mat-blue, 500);
}
}
.title {
font-weight: bold;
}
}
&.completed {
color: rgba(0, 0, 0, 0.87);
.index {
span {
border: 2px solid mat-color($mat-blue, 500);
}
&:after {
border-left-color: mat-color($mat-blue, 500);
}
}
+ .step {
.index {
&:before {
border-left-color: mat-color($mat-blue, 500);
}
}
}
}
&.first {
.index {
&:before {
display: none;
}
}
}
&.last {
.index {
&:after {
display: none;
}
}
}
.index {
display: flex;
margin-right: 12px;
span {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 28px;
min-width: 28px;
max-width: 28px;
height: 28px;
background: white;
border-radius: 100%;
border: 2px solid mat-color($mat-grey, 500);
font-weight: bold;
font-size: 12px;
z-index: 10;
}
&:before,
&:after {
position: absolute;
display: block;
content: ' ';
border-left: 1px solid mat-color($mat-grey, 300);
width: 1px;
height: 50%;
left: 29px;
z-index: 8;
}
&:before {
top: 0;
}
&:after {
bottom: 0;
}
}
.title {
display: flex;
}
}
}
}
.center {
position: relative;
.header {
height: 72px;
min-height: 72px;
max-height: 72px;
}
.content {
display: flex;
position: relative;
overflow: hidden;
height: 100%;
background: mat-color($mat-grey, 200);
.course-step {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: 48px;
@media (max-width: 720px) {
padding: 0 0 120px 0;
}
.course-step-content {
padding: 24px;
max-width: 720px;
margin: 0 auto;
width: 100%;
background: white;
@include mat-elevation(4);
}
}
}
.step-navigation {
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 32px;
max-width: 944px;
padding: 0 24px;
width: 100%;
left: 50%;
transform: translateX(-50%);
.previous {
margin-right: auto;
}
.next,
.done {
margin-left: auto;
}
}
}
}

View File

@@ -0,0 +1,101 @@
import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, QueryList, ViewChildren, ViewEncapsulation } from '@angular/core';
import { AcademyCourseService } from '../course.service';
import { Subscription } from 'rxjs/Subscription';
import { FusePerfectScrollbarDirective } from '../../../../../core/directives/fuse-perfect-scrollbar/fuse-perfect-scrollbar.directive';
import { fuseAnimations } from '../../../../../core/animations';
@Component({
selector : 'fuse-academy-course',
templateUrl : './course.component.html',
styleUrls : ['./course.component.scss'],
encapsulation: ViewEncapsulation.None,
animations : fuseAnimations
})
export class FuseAcademyCourseComponent implements OnInit, OnDestroy, AfterViewInit
{
course: any;
courseSubscription: Subscription;
currentStep = 0;
courseStepContent;
animationDirection: 'left' | 'right' | 'none' = 'none';
@ViewChildren(FusePerfectScrollbarDirective) fuseScrollbarDirectives: QueryList<FusePerfectScrollbarDirective>;
constructor(
private courseService: AcademyCourseService,
private changeDetectorRef: ChangeDetectorRef
)
{
}
ngOnInit()
{
// Subscribe to courses
this.courseSubscription =
this.courseService.onCourseChanged
.subscribe(course => {
this.course = course;
});
}
ngAfterViewInit()
{
this.courseStepContent = this.fuseScrollbarDirectives.find((fuseScrollbarDirective) => {
return fuseScrollbarDirective.element.nativeElement.id === 'course-step-content';
});
}
ngOnDestroy()
{
this.courseSubscription.unsubscribe();
}
gotoStep(step)
{
// Decide the animation direction
this.animationDirection = this.currentStep < step ? 'left' : 'right';
// Run change detection so the change
// in the animation direction registered
this.changeDetectorRef.detectChanges();
// Set the current step
this.currentStep = step;
}
gotoNextStep()
{
if ( this.currentStep === this.course.totalSteps - 1 )
{
return;
}
// Set the animation direction
this.animationDirection = 'left';
// Run change detection so the change
// in the animation direction registered
this.changeDetectorRef.detectChanges();
// Increase the current step
this.currentStep++;
}
gotoPreviousStep()
{
if ( this.currentStep === 0 )
{
return;
}
// Set the animation direction
this.animationDirection = 'right';
// Run change detection so the change
// in the animation direction registered
this.changeDetectorRef.detectChanges();
// Decrease the current step
this.currentStep--;
}
}

View File

@@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class AcademyCoursesService implements Resolve<any>
{
onCategoriesChanged: BehaviorSubject<any> = new BehaviorSubject({});
onCoursesChanged: BehaviorSubject<any> = new BehaviorSubject({});
constructor(private http: HttpClient)
{
}
/**
* The Academy App Main Resolver
*
* @param {ActivatedRouteSnapshot} route
* @param {RouterStateSnapshot} state
* @returns {Observable<any> | Promise<any> | any}
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> | Promise<any> | any
{
return new Promise((resolve, reject) => {
Promise.all([
this.getCategories(),
this.getCourses()
]).then(
() => {
resolve();
},
reject
);
});
}
getCategories(): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/academy-categories')
.subscribe((response: any) => {
this.onCategoriesChanged.next(response);
resolve(response);
}, reject);
});
}
getCourses(): Promise<any>
{
return new Promise((resolve, reject) => {
this.http.get('api/academy-courses')
.subscribe((response: any) => {
this.onCoursesChanged.next(response);
resolve(response);
}, reject);
});
}
}

View File

@@ -0,0 +1,97 @@
<div id="academy-courses" class="page-layout simple" fusePerfectScrollbar>
<!-- HEADER -->
<div class="header mat-accent-bg p-16 p-sm-24" fxLayout="column" fxLayoutAlign="center center">
<div class="hero-text">
<mat-icon class="hero-icon">school</mat-icon>
<h1>WELCOME TO ACADEMY</h1>
<h3>
Our courses will step you through the process of building a small application, or adding a new feature
to an existing application.
</h3>
</div>
</div>
<!-- / HEADER -->
<!-- CONTENT -->
<div class="content p-24">
<div fxLayout="column" fxLayoutAlign="center">
<div class="filters" fxLayout="column" fxLayoutAlign="center center"
fxLayout.gt-xs="row" fxLayoutAlign.gt-xs="space-between center">
<mat-form-field class="course-search">
<input matInput placeholder="Search for a course" [(ngModel)]="searchTerm"
(input)="filterCoursesByTerm()">
</mat-form-field>
<mat-form-field class="category-selector">
<mat-select placeholder="Select Category" [(ngModel)]="currentCategory"
(change)="filterCoursesByCategory()">
<mat-option [value]="'all'">
All
</mat-option>
<mat-option *ngFor="let category of categories" [value]="category.value">
{{ category.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="courses" fxLayout="row" fxLayoutAlign="center" fxLayoutWrap>
<div class="course" *ngFor="let course of filteredCourses" fxFlex="100" fxFlex.gt-xs="50"
fxFlex.gt-sm="33" [ngClass]="course.category">
<div class="course-content" fxLayout="column">
<div class="header" fxLayout="row" fxLayoutAlign="center center"
[ngClass]="course.category + '-bg'">
<div class="category" fxFlex>
{{course.category}}
</div>
<div class="length" fxLayout="row" fxLayoutAlign="center center">
<mat-icon class="length-icon s-20">access_time</mat-icon>
<div class="min">{{course.length}} min</div>
</div>
</div>
<div class="content" fxLayout="column" fxLayoutAlign="center center" fxFlex>
<div class="h1">{{course.title}}</div>
<div class="updated">Updated {{course.updated}}</div>
</div>
<div class="footer" fxLayout="row" fxLayoutAlign="center center">
<button mat-button color="accent"
[routerLink]="'/apps/academy/courses/' + course.id + '/' + course.slug">
START
</button>
</div>
</div>
</div>
<div class="no-courses" *ngIf="filteredCourses.length === 0">
No courses found!
</div>
</div>
</div>
</div>
<!-- / CONTENT -->
</div>

View File

@@ -0,0 +1,188 @@
@import "src/app/core/scss/fuse";
#academy-courses {
.header {
position: relative;
flex: 1 0 auto;
height: 280px;
max-height: 280px;
background: #1A237E;
background: linear-gradient(to right, #0E2A3B 0%, #34306B 100%);
text-align: center;
@include media-breakpoint('xs') {
height: 240px;
padding: 16px;
}
.hero-text {
.hero-icon {
position: absolute;
top: -64px;
left: 0px;
opacity: 0.04;
font-size: 512px !important;
width: 512px !important;
height: 512px !important;
}
h1 {
color: white;
font-size: 40px;
font-weight: 300;
letter-spacing: 0.01em;
text-align: center;
margin-top: 0;
margin-bottom: 16px;
@include media-breakpoint-down('xs') {
font-size: 24px;
}
}
h3 {
color: rgba(255, 255, 255, 0.75);
max-width: 480px;
text-align: center;
font-weight: 300;
letter-spacing: 0.03em;
margin: 0;
@include media-breakpoint-down('xs') {
font-size: 14px;
}
}
}
}
.content {
.category-selector {
min-width: 200px;
}
.filters {
width: 100%;
max-width: 1040px;
margin: 24px auto;
padding: 0 20px;
@include media-breakpoint-down('xs') {
margin: 0 auto;
}
.course-search {
width: 200px;
@include media-breakpoint-down('xs') {
margin-bottom: 16px;
}
@include media-breakpoint-up('sm') {
margin-right: 16px;
}
}
}
.courses {
width: 100%;
max-width: 1040px;
margin: 0 auto;
.no-courses {
font-size: 24px;
margin: 24px 0;
text-align: center;
}
.course {
padding: 16px;
&:hover {
.course-content {
@include mat-elevation(8);
}
}
.course-content {
background: white;
min-height: 240px;
transition: box-shadow 150ms ease-in-out;
@include mat-elevation(1);
.header {
color: white;
padding: 16px 24px;
height: 64px !important;
min-height: 64px !important;
max-height: 64px !important;
&.web-bg {
background: mat-color($mat-blue, 500);
}
&.android-bg {
background: mat-color($mat-green, 500);
}
&.firebase-bg {
background: mat-color($mat-amber, 800);
}
&.cloud-bg {
background: mat-color($mat-blue-grey, 500);
}
.category {
text-transform: capitalize;
text-align: left;
font-weight: 500;
color: rgba(0, 0, 0, 0.54);
}
.length {
.length-icon {
margin-right: 8px;
color: rgba(0, 0, 0, 0.54) !important;
}
.min {
font-size: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
}
.content {
padding: 24px;
.h1 {
font-size: 16px;
text-align: center;
}
.updated {
font-size: 13px;
font-weight: 500;
margin-top: 4px;
color: rgba(0, 0, 0, 0.37);
}
}
.footer {
padding: 16px;
height: 48px !important;
min-height: 48px !important;
max-height: 48px !important;
box-shadow: inset 0 1px 0 0 rgba(0, 0, 0, 0.12);
}
}
}
}
}
}

View File

@@ -0,0 +1,91 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { AcademyCoursesService } from '../courses.service';
@Component({
selector : 'fuse-academy-courses',
templateUrl: './courses.component.html',
styleUrls : ['./courses.component.scss']
})
export class FuseAcademyCoursesComponent implements OnInit, OnDestroy
{
categories: any[];
courses: any[];
coursesFilteredByCategory: any[];
filteredCourses: any[];
categoriesSubscription: Subscription;
coursesSubscription: Subscription;
currentCategory = 'all';
searchTerm = '';
constructor(
private coursesService: AcademyCoursesService
)
{
}
ngOnInit()
{
// Subscribe to categories
this.categoriesSubscription =
this.coursesService.onCategoriesChanged
.subscribe(categories => {
this.categories = categories;
});
// Subscribe to courses
this.coursesSubscription =
this.coursesService.onCoursesChanged
.subscribe(courses => {
this.filteredCourses = this.coursesFilteredByCategory = this.courses = courses;
});
}
ngOnDestroy()
{
this.categoriesSubscription.unsubscribe();
this.coursesSubscription.unsubscribe();
}
filterCoursesByCategory()
{
// Filter
if ( this.currentCategory === 'all' )
{
this.coursesFilteredByCategory = this.courses;
this.filteredCourses = this.courses;
}
else
{
this.coursesFilteredByCategory = this.courses.filter((course) => {
return course.category === this.currentCategory;
});
this.filteredCourses = [...this.coursesFilteredByCategory];
}
// Re-filter by search term
this.filterCoursesByTerm();
}
filterCoursesByTerm()
{
const searchTerm = this.searchTerm.toLowerCase();
// Search
if ( searchTerm === '' )
{
this.filteredCourses = this.coursesFilteredByCategory;
}
else
{
this.filteredCourses = this.coursesFilteredByCategory.filter((course) => {
return course.title.toLowerCase().includes(searchTerm);
});
}
}
}

View File

@@ -216,52 +216,52 @@
}
&.Jan {
background-image: url('/assets/images/backgrounds/january.jpg');
background-position: 0 45%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 85%;
}
&.Feb {
background-image: url('/assets/images/backgrounds/february.jpg');
background-position: 0 50%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 85%;
}
&.Mar {
background-image: url('/assets/images/backgrounds/march.jpg');
background-position: 0 45%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 40%;
}
&.Apr {
background-image: url('/assets/images/backgrounds/april.jpg');
background-position: 0 48%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 40%;
}
&.May {
background-image: url('/assets/images/backgrounds/may.jpg');
background-position: 0 47%;
background-image: url('/assets/images/backgrounds/spring.jpg');
background-position: 0 40%;
}
&.Jun {
background-image: url('/assets/images/backgrounds/june.jpg');
background-position: 0 48%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 80%;
}
&.Jul {
background-image: url('/assets/images/backgrounds/july.jpg');
background-position: 0 3%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 80%;
}
&.Aug {
background-image: url('/assets/images/backgrounds/august.jpg');
background-position: 0 61%;
background-image: url('/assets/images/backgrounds/summer.jpg');
background-position: 0 80%;
}
&.Sep {
background-image: url('/assets/images/backgrounds/september.jpg');
background-position: 0 58%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Oct {
background-image: url('/assets/images/backgrounds/october.jpg');
background-position: 0 50%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Nov {
background-image: url('/assets/images/backgrounds/november.jpg');
background-position: 0 46%;
background-image: url('/assets/images/backgrounds/autumn.jpg');
background-position: 0 40%;
}
&.Dec {
background-image: url('/assets/images/backgrounds/december.jpg');
background-position: 0 43%;
background-image: url('/assets/images/backgrounds/winter.jpg');
background-position: 0 85%;
}
.header-content {
@@ -299,7 +299,7 @@
.add-event-button {
position: absolute;
right: 18px;
bottom: -32px;
bottom: -26px;
z-index: 10;
}

View File

@@ -32,9 +32,11 @@
</mat-slide-toggle>
</div>
<div class="py-16" fxFlex="1 0 auto" fxLayout="row" formGroupName="color">
<mat-form-field class="mr-24" fxFlex>
<div class="py-16" fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row" formGroupName="color">
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput
class="primary-color-input"
name="primary color"
formControlName="primary"
placeholder="Primary color"
@@ -44,8 +46,10 @@
[style.background]="event.color.primary"
(colorPickerChange)="event.color.primary = $event; eventForm.patchValue({color:{primary:$event}})"/>
</mat-form-field>
<mat-form-field fxFlex>
<input matInput
class="secondary-color-input"
name="secondary color"
formControlName="secondary"
placeholder="Secondary color"
@@ -55,11 +59,12 @@
[style.background]="event.color.secondary"
(colorPickerChange)="event.color.secondary = $event; eventForm.patchValue({color:{secondary:$event}})"/>
</mat-form-field>
</div>
<div fxFlex="1 0 auto" fxLayout="row">
<div fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row">
<mat-form-field class="mr-24" fxFlex>
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput [matDatepicker]="startDatePicker" placeholder="Start Date"
name="start"
formControlName="start">
@@ -73,9 +78,9 @@
</div>
<div fxFlex="1 0 auto" fxLayout="row">
<div fxFlex="1 0 auto" fxLayout="column" fxLayout.gt-xs="row">
<mat-form-field class="mr-24" fxFlex>
<mat-form-field class="mr-sm-24" fxFlex>
<input matInput [matDatepicker]="endDatePicker" placeholder="End Date"
name="end"
formControlName="end">
@@ -98,12 +103,12 @@
<mat-form-field formGroupName="meta" class="w-100-p">
<textarea matInput
formControlName="notes"
placeholder="Notes"
mat-maxlength="250"
max-rows="4">
</textarea>
<textarea matInput
formControlName="notes"
placeholder="Notes"
mat-maxlength="250"
max-rows="4">
</textarea>
</mat-form-field>
</form>

View File

@@ -1,8 +1,17 @@
@import "src/app/core/scss/fuse";
.event-form-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('xs') {
width: 480px;
}
.mat-dialog-container {
padding: 0;
width: 480px;
}
.dialog-content-wrapper {
@@ -10,4 +19,9 @@
display: flex;
flex-direction: column;
}
.primary-color-input,
.secondary-color-input {
padding: 6px 8px;
}
}

View File

@@ -2,15 +2,18 @@
.contact-form-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('xs') {
width: 400px;
}
.mat-dialog-container {
padding: 0;
width: 400px;
overflow: hidden;
@include media-breakpoint('xs') {
width: auto;
}
.mat-toolbar {
min-height: initial;
}

View File

@@ -10,7 +10,7 @@ import { Subject } from 'rxjs/Subject';
@Injectable()
export class ContactsService implements Resolve<any>
{
onContactsChanged: BehaviorSubject<any> = new BehaviorSubject({});
onContactsChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onSelectedContactsChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onUserDataChanged: BehaviorSubject<any> = new BehaviorSubject([]);
onSearchTextChanged: Subject<any> = new Subject();

View File

@@ -703,7 +703,7 @@
<div class="pl-16 pr-8 py-16" fxLayout="row" fxLayoutAlign="space-between center">
<div class="h3">{{dateNow | date: 'EEEE, h:m:ss'}}</div>
<div class="h3">{{dateNow | date: 'EEEE, HH:mm:ss'}}</div>
<div>
<button mat-icon-button [matMenuTriggerFor]="moreMenu" aria-label="more">

View File

@@ -1,9 +1,5 @@
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceDashboardService } from './dashboard.service';
import * as shape from 'd3-shape';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { DataSource } from '@angular/cdk/collections';
import { fuseAnimations } from '../../../../../core/animations';
@Component({
@@ -23,14 +19,10 @@ export class FuseEcommerceDashboardComponent implements OnInit, OnDestroy
widget6: any = {};
widget7: any = {};
dateNow = Date.now();
constructor(private projectsDashboardService: EcommerceDashboardService)
{
this.projects = this.projectsDashboardService.projects;
this.selectedProject = this.projects[0];
this.widgets = this.projectsDashboardService.widgets;
/**

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceOrderService } from './order.service';
import { fuseAnimations } from '../../../../../core/animations';
import 'rxjs/add/operator/startWith';
@@ -10,9 +10,6 @@ import 'rxjs/add/observable/fromEvent';
import { Subscription } from 'rxjs/Subscription';
import { Order } from './order.model';
import { FormBuilder, FormGroup } from '@angular/forms';
import { FuseUtils } from '../../../../../core/fuseUtils';
import { MatSnackBar } from '@angular/material';
import { Location } from '@angular/common';
import { orderStatuses } from './order-statuses';
@Component({
@@ -32,8 +29,6 @@ export class FuseEcommerceOrderComponent implements OnInit, OnDestroy
constructor(
private orderService: EcommerceOrderService,
private formBuilder: FormBuilder,
public snackBar: MatSnackBar,
private location: Location
)
{

View File

@@ -17,6 +17,7 @@
.orders-table {
flex: 1 1 auto;
border-bottom: 1px solid rgba(0, 0, 0, .12);
overflow: auto;
.mat-header-row {
min-height: 64px;

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
import { EcommerceProductService } from './product.service';
import { fuseAnimations } from '../../../../../core/animations';
import 'rxjs/add/operator/startWith';

View File

@@ -24,6 +24,7 @@
.products-table {
flex: 1 1 auto;
border-bottom: 1px solid rgba(0, 0, 0, .12);
overflow: auto;
.mat-header-row {
min-height: 64px;

View File

@@ -0,0 +1,111 @@
<div class="dialog-content-wrapper">
<mat-toolbar matDialogTitle class="mat-accent m-0">
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center">
<span class="title dialog-title">New Message</span>
<button mat-button class="mat-icon-button"
(click)="dialogRef.close()"
aria-label="Close dialog">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div mat-dialog-content class="p-24 m-0" fusePerfectScrollbar>
<form name="composeForm" [formGroup]="composeForm" class="compose-form" fxLayout="column" fxFlex>
<mat-form-field>
<input matInput name="from"
placeholder="From"
formControlName="from"
type="email">
</mat-form-field>
<mat-form-field>
<input matInput name="to"
placeholder="To"
formControlName="to"
type="email" required>
</mat-form-field>
<mat-form-field>
<input matInput
name="cc"
placeholder="Cc"
formControlName="cc"
type="email">
</mat-form-field>
<mat-form-field>
<input matInput
name="bcc"
placeholder="Bcc"
formControlName="bcc"
type="email">
</mat-form-field>
<mat-form-field>
<input matInput name="subject"
placeholder="Subject"
formControlName="subject">
</mat-form-field>
<mat-form-field>
<textarea matInput name="message"
placeholder="Message"
formControlName="message"
rows="6">
</textarea>
</mat-form-field>
<div class="attachment-list">
<div class="attachment" fxLayout="row" fxLayoutAlign="space-between center">
<div>
<span class="filename">attachment-2.doc</span>
<span class="size">(12 Kb)</span>
</div>
<button mat-icon-button aria-label="Delete attachment">
<mat-icon class="s-16">close</mat-icon>
</button>
</div>
<div class="attachment" fxLayout="row" fxLayoutAlign="space-between center">
<div>
<span class="filename">attachment-1.jpg</span>
<span class="size">(350 Kb)</span>
</div>
<button mat-icon-button aria-label="Delete attachment">
<mat-icon class="s-16">close</mat-icon>
</button>
</div>
</div>
</form>
</div>
<div mat-dialog-actions class="m-0 p-16" fxLayout="row" fxLayoutAlign="space-between center">
<div>
<button mat-raised-button
(click)="dialogRef.close(['send',composeForm])"
class="save-button mat-accent"
[disabled]="composeForm.invalid"
aria-label="SAVE">
SEND
</button>
<button mat-icon-button matTooltip="Attach a file">
<mat-icon>attach_file</mat-icon>
</button>
</div>
<button mat-button
class="mat-icon-button"
(click)="dialogRef.close(['delete',composeForm])"
aria-label="Delete"
matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>

View File

@@ -0,0 +1,40 @@
.mail-compose-dialog {
.mat-dialog-container {
padding: 0;
width: 720px;
.compose-form {
.mat-form-field {
width: 100%;
}
.attachment-list {
font-size: 13px;
padding-top: 16px;
.attachment {
background-color: rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.16);
padding-left: 16px;
margin-top: 8px;
border-radius: 2px;
.filename {
font-weight: 500;
}
&:last-child {
margin-bottom: 0;
}
}
}
}
}
.dialog-content-wrapper {
max-height: 85vh;
display: flex;
flex-direction: column;
}
}

View File

@@ -0,0 +1,44 @@
import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector : 'fuse-mail-compose',
templateUrl : './compose.component.html',
styleUrls : ['./compose.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FuseMailNgrxComposeDialogComponent implements OnInit
{
composeForm: FormGroup;
constructor(
public dialogRef: MatDialogRef<FuseMailNgrxComposeDialogComponent>,
@Inject(MAT_DIALOG_DATA) private data: any,
private formBuilder: FormBuilder
)
{
this.composeForm = this.createComposeForm();
}
ngOnInit()
{
}
createComposeForm()
{
return this.formBuilder.group({
from : {
value : ['johndoe@creapond.com'],
disabled: [true]
},
to : [''],
cc : [''],
bcc : [''],
subject: [''],
message: ['']
});
}
}

View File

@@ -0,0 +1,14 @@
export const locale = {
lang: 'en',
data: {
'MAIL': {
'COMPOSE' : 'COMPOSE',
'FOLDERS' : 'FOLDERS',
'FILTERS' : 'FILTERS',
'LABELS' : 'LABELS',
'NO_MESSAGES' : 'There are no messages!',
'SELECT_A_MESSAGE_TO_READ': 'Select a message to read',
'SEARCH_PLACEHOLDER': 'Search for an e-mail or contact'
}
}
};

View File

@@ -0,0 +1,14 @@
export const locale = {
lang: 'tr',
data: {
'MAIL': {
'COMPOSE' : 'YENİ E-POSTA',
'FOLDERS' : 'KLASÖRLER',
'FILTERS' : 'FİLTRELER',
'LABELS' : 'ETİKETLER',
'NO_MESSAGES' : 'Mesajiniz bulunmamakta!',
'SELECT_A_MESSAGE_TO_READ': 'Okumak için bir mesaj seçin',
'SEARCH_PLACEHOLDER' : 'E-mail yada bir kişi arayın'
}
}
};

View File

@@ -0,0 +1,140 @@
<div *ngIf="!mail" fxLayout="column" fxLayoutAlign="center center" fxFlex>
<mat-icon class="s-128 mb-16 select-message-icon">
email
</mat-icon>
<span class="select-message-text hint-text">
<span>{{ 'MAIL.SELECT_A_MESSAGE_TO_READ' | translate }}</span>
</span>
</div>
<div *ngIf="mail">
<div class="mail-header" fxLayout="row" fxLayoutAlign="space-between center">
<div>
<div class="subject" flex>{{mail.subject}}</div>
<div class="labels" fxLayout="row" fxLayoutWrap>
<div class="label" *ngFor="let labelId of mail.labels"
fxLayout="row" fxLayoutAlign="start center">
<div class="label-color" [ngStyle]="{'background-color': (labels$ | async) | getById:labelId:'color'}"></div>
<div class="label-title">{{(labels$ | async) | getById:labelId:'title'}}</div>
</div>
</div>
</div>
<div class="actions" fxLayout="row" fxLayoutAlign="start center">
<button mat-button class="mat-icon-button" (click)="toggleStar($event)" aria-label="Toggle star">
<mat-icon *ngIf="mail.starred">star</mat-icon>
<mat-icon *ngIf="!mail.starred">star_outline</mat-icon>
</button>
<button mat-button class="mat-icon-button" (click)="toggleImportant($event)" aria-label="Toggle important">
<mat-icon *ngIf="mail.important">label</mat-icon>
<mat-icon *ngIf="!mail.important">label_outline</mat-icon>
</button>
</div>
</div>
<div class="mail-content">
<div class="info" fxLayout="row" fxLayoutAlign="space-between start">
<div fxFlex fxLayout="column" fxLayoutAlign="start start">
<div fxLayout="row" fxLayoutAlign="start start">
<div>
<img *ngIf="mail.from.avatar" alt="{{mail.from.name}}"
src="{{mail.from.avatar}}" class="avatar"/>
<div *ngIf="!mail.from.avatar" class="avatar" ms-random-class="vm.colors">
{{mail.from.name[0]}}
</div>
</div>
<div fxLayout="column" fxLayoutAlign="start start">
<div class="name">
{{mail.from.name}}
</div>
<div class="to" fxLayout="row" fxLayoutAlign="start center">
<div class="to-text">to</div>
<div>{{mail.to[0].name}}</div>
</div>
</div>
</div>
<a class="toggle-details" (click)="showDetails = !showDetails">
<span *ngIf="!showDetails">Show Details</span>
<span *ngIf="showDetails">Hide Details</span>
</a>
<div *ngIf="showDetails" class="details" fxLayout="row" fxLayoutAlign="start start">
<div fxLayout="column">
<span class="title">From:</span>
<span class="title">To:</span>
<span class="title">Date:</span>
</div>
<div fxLayout="column">
<span class="detail">{{mail.from.email}}</span>
<span class="detail">{{mail.to[0].email}}</span>
<span class="detail">{{mail.time}}</span>
</div>
</div>
</div>
<button mat-button [matMenuTriggerFor]="moreMenu" aria-label="More" class="mat-icon-button"
(click)="$event.stopPropagation()">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #moreMenu="matMenu">
<button mat-menu-item aria-label="Reply">
<mat-icon>reply</mat-icon>
<span>Reply</span>
</button>
<button mat-menu-item aria-label="Forward">
<mat-icon>forward</mat-icon>
<span>Forward</span>
</button>
<button mat-menu-item aria-label="Print">
<mat-icon>print</mat-icon>
<span>Print</span>
</button>
</mat-menu>
</div>
<div [innerHTML]="mail.message"></div>
</div>
<div *ngIf="mail.attachments" class="mail-attachments">
<div class="title">
<span>Attachments</span>
({{mail.attachments.length}})
</div>
<div class="attachment-list" fxLayout="row" fxLayoutWrap>
<div class="attachment" fxLayout="column"
*ngFor="let attachment of mail.attachments">
<img class="preview" src="{{attachment.preview}}">
<div fxLayout="column">
<a href="#" onclick="event.preventDefault()">View</a>
<a href="#" onclick="event.preventDefault()">Download</a>
<div class="size">({{attachment.size}})</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,118 @@
@import 'src/app/core/scss/fuse';
:host {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
padding: 24px;
.select-message-text {
font-size: 24px;
font-weight: 300;
}
.mail-header {
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
.actions {
min-width: 88px;
}
.subject {
font-size: 17px;
font-weight: 500;
}
.label {
font-size: 11px;
border-radius: 2px;
margin: 4px 4px 4px 0;
padding: 3px 8px;
background-color: rgba(0, 0, 0, 0.08);
.label-color {
width: 8px;
height: 8px;
margin-right: 8px;
border-radius: 50%;
}
}
}
.mail-content {
padding: 24px 0;
.to {
color: rgba(0, 0, 0, 0.54);
.to-text {
margin-right: 4px;
text-transform: lowercase;
}
}
.info {
padding-bottom: 16px;
.avatar {
margin-right: 16px;
background-color: mat-color($accent);
}
.name {
margin-right: 8px;
font-weight: 500;
}
.toggle-details {
user-select: none;
text-decoration: underline;
padding-top: 16px;
cursor: pointer;
font-weight: 500;
}
.details {
padding-top: 8px;
.title {
font-weight: 500;
margin-right: 6px;
}
.detail {
color: rgba(0, 0, 0, 0.54);
}
}
}
}
.mail-attachments {
padding: 24px 0;
border-top: 1px solid rgba(0, 0, 0, 0.12);
.title {
margin-bottom: 16px;
font-weight: 500;
}
.attachment {
.preview {
width: 100px;
margin: 0 16px 8px 0;
}
.link {
margin-bottom: 2px;
}
.size {
font-size: 11px;
}
}
}
}

View File

@@ -0,0 +1,77 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Mail } from '../mail.model';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import * as fromStore from '../store';
import { MailNgrxService } from '../mail.service';
@Component({
selector : 'fuse-mail-details',
templateUrl : './mail-details.component.html',
styleUrls : ['./mail-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FuseMailNgrxDetailsComponent implements OnInit, OnDestroy, OnChanges
{
labels$: Observable<any>;
@Input('mail') mailInput: Mail;
mail: Mail;
showDetails = false;
constructor(
private mailService: MailNgrxService,
private store: Store<fromStore.MailAppState>
)
{
this.labels$ = this.store.select(fromStore.getLabelsArr);
}
ngOnInit()
{
}
ngOnChanges()
{
this.updateModel(this.mailInput);
this.markAsRead();
}
markAsRead()
{
if ( this.mail && !this.mail.read )
{
this.mail.markRead();
this.updateMail();
}
}
toggleStar(event)
{
event.stopPropagation();
this.mail.toggleStar();
this.updateMail();
}
toggleImportant(event)
{
event.stopPropagation();
this.mail.toggleImportant();
this.updateMail();
}
updateModel(data)
{
this.mail = !data ? null : new Mail({...data});
}
updateMail()
{
this.store.dispatch(new fromStore.UpdateMail(this.mail));
this.updateModel(this.mail);
}
ngOnDestroy()
{
}
}

View File

@@ -0,0 +1,48 @@
<div fxLayout="row" fxLayoutAlign="start center">
<mat-checkbox [checked]="selected" (change)="onSelectedChange()"
(click)="$event.stopPropagation();">
</mat-checkbox>
<div class="info" fxFlex FlexLayout="column">
<div class="row-1" fxLayout="row" fxLayoutAlign="start center">
<div class="name" fxLayout="row" fxLayoutAlign="start center" fxFlex>
<img class="avatar" *ngIf="mail.from?.avatar" alt="{{mail.from?.name}}" src="{{mail.from?.avatar}}"/>
<div class="avatar" *ngIf="!mail.from?.avatar">{{mail.from?.name[0]}}</div>
<span class="text-truncate" *ngIf="mail?.from">{{mail.from?.name}}</span>
<mat-icon *ngIf="mail.hasAttachments">attachment</mat-icon>
</div>
<div fxLayout="row" fxLayoutAlign="start center">
<div class="time">{{mail.time}}</div>
</div>
</div>
<div class="row-2" fxLayout="row" fxLayoutAlign="start center">
<div fxLayout="column" fxLayoutAlign="start start">
<div class="subject text-truncate">
{{mail.subject}}
</div>
<div class="message text-truncate" *ngIf="mail?.message">
{{mail.message | htmlToPlaintext | slice:0:180}}{{mail.message.length > 180 ? '...' : ''}}
</div>
<div class="labels" fxLayout="row" fxLayoutWrap fxHide fxShow.gt-sm>
<div class="label" *ngFor="let labelId of mail.labels"
fxLayout="row" fxLayoutAlign="start center">
<div class="label-color"
[ngStyle]="{'background-color': (labels$ | async) | getById:labelId:'color'}"></div>
<div class="label-title">{{(labels$ | async) | getById:labelId:'title'}}</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,130 @@
@import 'src/app/core/scss/fuse';
:host {
flex-shrink: 0;
position: relative;
padding: 16px 24px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
cursor: pointer;
background: #FAFAFA;
&.unread {
background: #FFFFFF;
.info {
.name {
font-weight: 700;
}
.row-2 {
.subject {
font-weight: 700;
}
.labels {
background: #FFFFFF;
}
}
}
}
&.selected {
background: #FFF8E1;
.info {
.row-2 {
.labels {
background: #FFF8E1;
}
}
}
}
&.current-mail {
background: #E3F2FD;
.info {
.row-2 {
.labels {
background: #E3F2FD;
}
}
}
}
.info {
overflow: hidden;
width: 0;
margin-left: 16px;
position: relative;
.row-1 {
margin-bottom: 8px;
.name {
font-size: 15px;
font-weight: 500;
.avatar {
min-width: 32px;
width: 32px;
height: 32px;
line-height: 32px;
background-color: mat-color($accent);
}
}
.actions {
margin-left: 4px;
.mat-icon-button {
width: 32px;
height: 32px;
line-height: 1;
}
}
}
.row-2 {
.subject,
.message {
width: 100%;
}
.message {
position: relative;
color: rgba(0, 0, 0, 0.54);
}
.labels {
position: absolute;
background: #FFFFFF;
bottom: 0;
right: 0;
padding-left: 6px;
.label {
font-size: 11px;
border-radius: 2px;
margin: 0 4px 0 0;
padding: 3px 8px;
background-color: rgba(0, 0, 0, 0.08);
.label-color {
width: 8px;
height: 8px;
margin-right: 8px;
border-radius: 50%;
}
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core';
import { Mail } from '../../mail.model';
import { MailNgrxService } from '../../mail.service';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import * as fromStore from '../../store';
@Component({
selector : 'fuse-mail-list-item',
templateUrl : './mail-list-item.component.html',
styleUrls : ['./mail-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FuseMailNgrxListItemComponent implements OnInit, OnDestroy
{
@Input() mail: Mail;
@HostBinding('class.selected') selected: boolean;
@HostBinding('class.unread') unread: boolean;
labels$: Observable<any>;
selectedMailIds$: Observable<any>;
constructor(
private mailService: MailNgrxService,
private store: Store<fromStore.MailAppState>,
private cd: ChangeDetectorRef
)
{
this.labels$ = this.store.select(fromStore.getLabelsArr);
this.selectedMailIds$ = this.store.select(fromStore.getSelectedMailIds);
this.selected = false;
}
ngOnInit()
{
// Set the initial values
this.mail = new Mail(this.mail);
this.unread = !this.mail.read;
this.selectedMailIds$.subscribe((selectedMailIds) => {
this.selected = selectedMailIds.length > 0 && selectedMailIds.find(id => id === this.mail.id) !== undefined;
this.refresh();
});
}
refresh()
{
this.cd.markForCheck();
}
onSelectedChange()
{
this.store.dispatch(new fromStore.ToggleInSelectedMails(this.mail.id));
}
ngOnDestroy()
{
}
}

View File

@@ -0,0 +1,9 @@
<div *ngIf="mails.length === 0" fxLayout="column" fxLayoutAlign="center center" fxFlexFill>
<span class="no-messages-text hint-text">{{ 'MAIL.NO_MESSAGES' | translate }}</span>
</div>
<div class="mail-list">
<fuse-mail-list-item matRipple *ngFor="let mail of mails" [mail]="mail" (click)="readMail(mail.id)"
[ngClass]="{'current-mail':mail?.id == currentMail?.id}">
</fuse-mail-list-item>
</div>

View File

@@ -0,0 +1,21 @@
:host {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
overflow-y: auto;
padding: 0;
border-right: 1px solid rgba(0, 0, 0, .12);
.no-messages-text {
font-size: 24px;
font-weight: 300;
}
.mail-list {
position: relative;
display: flex;
flex-direction: column;
flex: 1;
}
}

View File

@@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Mail } from '../mail.model';
import { ActivatedRoute, Router } from '@angular/router';
import { MailNgrxService } from '../mail.service';
@Component({
selector : 'fuse-mail-list',
templateUrl : './mail-list.component.html',
styleUrls : ['./mail-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FuseMailNgrxListComponent implements OnInit, OnDestroy
{
@Input() mails: Mail[];
@Input() currentMail: Mail[];
constructor(
private route: ActivatedRoute,
private mailService: MailNgrxService,
private router: Router
)
{
}
ngOnInit()
{
}
/**
* Read mail
* @param mailId
*/
readMail(mailId)
{
const labelHandle = this.route.snapshot.params.labelHandle,
filterHandle = this.route.snapshot.params.filterHandle,
folderHandle = this.route.snapshot.params.folderHandle;
if ( labelHandle )
{
this.router.navigate(['apps/mail-ngrx/label/' + labelHandle + '/' + mailId]);
}
else if ( filterHandle )
{
this.router.navigate(['apps/mail-ngrx/filter/' + filterHandle + '/' + mailId]);
}
else
{
this.router.navigate(['apps/mail-ngrx/' + folderHandle + '/' + mailId]);
}
}
ngOnDestroy()
{
}
}

View File

@@ -0,0 +1,116 @@
<div id="mail" class="page-layout carded left-sidenav" fusePerfectScrollbar>
<!-- TOP BACKGROUND -->
<div class="top-bg mat-accent-bg"></div>
<!-- / TOP BACKGROUND -->
<mat-sidenav-container>
<!-- SIDENAV -->
<mat-sidenav class="sidenav mat-sidenav-opened" align="start" mode="side" opened="true"
fuseMatSidenavHelper="carded-left-sidenav" mat-is-locked-open="gt-md">
<fuse-mail-main-sidenav></fuse-mail-main-sidenav>
</mat-sidenav>
<!-- / SIDENAV -->
<!-- CENTER -->
<div class="center">
<!-- CONTENT HEADER -->
<div class="header" fxLayout="row" fxLayoutAlign="start center">
<div class="search-wrapper" fxFlex fxLayout="row" fxLayoutAlign="start center">
<button mat-button class="mat-icon-button sidenav-toggle"
fuseMatSidenavToggler="carded-left-sidenav"
fxHide.gt-md aria-label="Toggle Sidenav">
<mat-icon>menu</mat-icon>
</button>
<div class="search mat-white-bg" flex fxLayout="row" fxLayoutAlign="start center">
<mat-icon>search</mat-icon>
<input [formControl]="searchInput" [placeholder]="'MAIL.SEARCH_PLACEHOLDER' | translate" fxFlex>
</div>
</div>
</div>
<!-- / CONTENT HEADER -->
<!-- CONTENT CARD -->
<div class="content-card mat-white-bg" [ngClass]="{'current-mail-selected':currentMail$ | async}">
<!-- CONTENT TOOLBAR -->
<div class="toolbar px-24 py-8">
<div class="mail-selection" fxFlex="row" fxLayoutAlign="start center">
<mat-checkbox (click)="toggleSelectAll($event)"
[checked]="hasSelectedMails"
[indeterminate]="isIndeterminate">
</mat-checkbox>
<button mat-icon-button [matMenuTriggerFor]="selectMenu">
<mat-icon>arrow_drop_down</mat-icon>
</button>
<mat-menu #selectMenu="matMenu">
<button mat-menu-item (click)="selectAllMails()">All</button>
<button mat-menu-item (click)="deselectAllMails()">None</button>
<button mat-menu-item (click)="selectMailsByParameter('read', true)">Read</button>
<button mat-menu-item (click)="selectMailsByParameter('read', false)">Unread</button>
<button mat-menu-item (click)="selectMailsByParameter('starred', true)">Starred</button>
<button mat-menu-item (click)="selectMailsByParameter('starred', false)">Unstarred</button>
<button mat-menu-item (click)="selectMailsByParameter('important', true)">Important</button>
<button mat-menu-item (click)="selectMailsByParameter('important', false)">Unimportant</button>
</mat-menu>
<div class="toolbar-separator" *ngIf="hasSelectedMails"></div>
<button mat-icon-button (click)="setFolderOnSelectedMails(4)" *ngIf="hasSelectedMails">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="folderMenu" *ngIf="hasSelectedMails">
<mat-icon>folder</mat-icon>
</button>
<mat-menu #folderMenu="matMenu">
<button mat-menu-item *ngFor="let folder of folders$ | async"
(click)="setFolderOnSelectedMails(folder.id)">{{folder.title}}
</button>
</mat-menu>
<button mat-icon-button [matMenuTriggerFor]="labelMenu" *ngIf="hasSelectedMails">
<mat-icon>label</mat-icon>
</button>
<mat-menu #labelMenu="matMenu">
<button mat-menu-item *ngFor="let label of labels$ | async"
(click)="toggleLabelOnSelectedMails(label.id)">{{label.title}}
</button>
</mat-menu>
</div>
<div *ngIf="currentMail$ | async" fxHide.gt-xs>
<button mat-icon-button (click)="deSelectCurrentMail()">
<mat-icon>arrow_back</mat-icon>
</button>
</div>
</div>
<!-- / CONTENT TOOLBAR -->
<!-- CONTENT -->
<div class="content" fxLayoutAlign="row">
<fuse-mail-list fusePerfectScrollbar fxFlex [mails]="mails$ | async" [currentMail]="currentMail$ | async"></fuse-mail-list>
<fuse-mail-details [mail]="currentMail$ | async" fusePerfectScrollbar fxFlex></fuse-mail-details>
</div>
<!-- / CONTENT -->
</div>
<!-- / CONTENT CARD -->
</div>
<!-- / CENTER -->
</mat-sidenav-container>
</div>

View File

@@ -0,0 +1,81 @@
@import "src/app/core/scss/fuse";
:host {
width: 100%;
.center {
.header {
.search-wrapper {
@include mat-elevation(7);
.sidenav-toggle {
margin: 0;
width: 56px;
height: 56px;
background: #FFF;
border-radius: 0;
border-right: 1px solid rgba(0, 0, 0, .12);
}
.search {
width: 100%;
height: 56px;
line-height: 56px;
padding: 18px;
input {
height: 56px;
padding-left: 16px;
color: rgba(0, 0, 0, 0.54);
border: none;
outline: none;
}
}
}
}
.content-card {
@include media-breakpoint(xs) {
fuse-mail-list {
border-right: none;
}
fuse-mail-list,
fuse-mail-details {
flex: 1 0 100%;
}
fuse-mail-details {
display: none !important;
}
&.current-mail-selected {
.toolbar {
padding-left: 16px !important;
.mail-selection {
display: none !important;
}
}
.content {
fuse-mail-list {
display: none !important;
}
fuse-mail-details {
display: flex !important;
}
}
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MailNgrxService } from './mail.service';
import { FormControl } from '@angular/forms';
import { Mail } from './mail.model';
import { FuseTranslationLoaderService } from '../../../../core/services/translation-loader.service';
import { locale as english } from './i18n/en';
import { locale as turkish } from './i18n/tr';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import * as fromStore from './store';
import { FuseConfigService } from '../../../../core/services/config.service';
@Component({
selector : 'fuse-mail',
templateUrl : './mail.component.html',
styleUrls : ['./mail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FuseMailNgrxComponent implements OnInit, OnDestroy
{
hasSelectedMails: boolean;
isIndeterminate: boolean;
searchInput: FormControl;
mails$: Observable<any>;
folders$: Observable<any>;
labels$: Observable<any>;
currentMail$: Observable<Mail>;
selectedMailIds$: Observable<string[]>;
searchText$: Observable<string>;
mails: Mail[];
selectedMailIds: string[];
constructor(
private configService: FuseConfigService,
private mailService: MailNgrxService,
private translationLoader: FuseTranslationLoaderService,
private store: Store<fromStore.MailAppState>,
private cd: ChangeDetectorRef
)
{
this.searchInput = new FormControl('');
this.translationLoader.loadTranslations(english, turkish);
this.currentMail$ = this.store.select(fromStore.getCurrentMail);
this.mails$ = this.store.select(fromStore.getMailsArr);
this.folders$ = this.store.select(fromStore.getFoldersArr);
this.labels$ = this.store.select(fromStore.getLabelsArr);
this.selectedMailIds$ = this.store.select(fromStore.getSelectedMailIds);
this.searchText$ = this.store.select(fromStore.getSearchText);
this.mails = [];
this.selectedMailIds = [];
this.configService.setSettings({
routerAnimation: 'none'
});
}
ngOnInit()
{
this.mails$.subscribe(mails => {
this.mails = mails;
});
this.selectedMailIds$
.subscribe(selectedMailIds => {
this.selectedMailIds = selectedMailIds;
this.hasSelectedMails = selectedMailIds.length > 0;
this.isIndeterminate = (selectedMailIds.length !== this.mails.length && selectedMailIds.length > 0);
this.refresh();
});
this.searchText$.subscribe(searchText => {
this.searchInput.setValue(searchText);
});
this.searchInput.valueChanges
.debounceTime(300)
.distinctUntilChanged()
.subscribe(searchText => {
this.store.dispatch(new fromStore.SetSearchText(searchText));
});
}
toggleSelectAll(ev)
{
ev.preventDefault();
if ( this.selectedMailIds.length && this.selectedMailIds.length > 0 )
{
this.deselectAllMails();
}
else
{
this.selectAllMails();
}
}
selectAllMails()
{
this.store.dispatch(new fromStore.SelectAllMails());
}
deselectAllMails()
{
this.store.dispatch(new fromStore.DeselectAllMails());
}
selectMailsByParameter(parameter, value)
{
this.store.dispatch(new fromStore.SelectMailsByParameter({
parameter,
value
}));
}
toggleLabelOnSelectedMails(labelId)
{
this.store.dispatch(new fromStore.AddLabelOnSelectedMails(labelId));
}
setFolderOnSelectedMails(folderId)
{
this.store.dispatch(new fromStore.SetFolderOnSelectedMails(folderId));
}
deSelectCurrentMail()
{
this.store.dispatch(new fromStore.SetCurrentMail(''));
}
refresh()
{
this.cd.markForCheck();
}
ngOnDestroy()
{
this.cd.detach();
}
}

View File

@@ -0,0 +1,66 @@
export class Mail
{
id: string;
from: {
name: string,
avatar: string,
email: string
};
to: {
name: string,
email: string
}[];
subject: string;
message: string;
time: string;
read: boolean;
starred: boolean;
important: boolean;
hasAttachments: boolean;
attachments: {
type: string,
fileName: string,
preview: string,
url: string,
size: string
}[];
labels: string[];
folder: string;
constructor(mail)
{
this.id = mail.id;
this.from = mail.from;
this.to = mail.to;
this.subject = mail.subject;
this.message = mail.message;
this.time = mail.time;
this.read = mail.read;
this.starred = mail.starred;
this.important = mail.important;
this.hasAttachments = mail.hasAttachments;
this.attachments = mail.attachments;
this.labels = mail.labels;
this.folder = mail.folder;
}
toggleStar()
{
this.starred = !this.starred;
}
toggleImportant()
{
this.important = !this.important;
}
markRead()
{
this.read = true;
}
markUnRead()
{
this.read = false;
}
}

View File

@@ -0,0 +1,73 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '../../../../core/modules/shared.module';
import { RouterModule, Routes } from '@angular/router';
import { FuseMailNgrxComponent } from './mail.component';
import { FuseMailNgrxMainSidenavComponent } from './sidenavs/main/main-sidenav.component';
import { FuseMailNgrxListItemComponent } from './mail-list/mail-list-item/mail-list-item.component';
import { FuseMailNgrxListComponent } from './mail-list/mail-list.component';
import { FuseMailNgrxDetailsComponent } from './mail-details/mail-details.component';
import { MailNgrxService } from './mail.service';
import { FuseMailNgrxComposeDialogComponent } from './dialogs/compose/compose.component';
import { MailAppStoreModule } from './store/store.module';
import * as fromGuards from './store/guards/index';
const routes: Routes = [
{
path : 'label/:labelHandle',
component : FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : 'label/:labelHandle/:mailId',
component : FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : 'filter/:filterHandle',
component: FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : 'filter/:filterHandle/:mailId',
component: FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : ':folderHandle',
component: FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : ':folderHandle/:mailId',
component: FuseMailNgrxComponent,
canActivate: [fromGuards.ResolveGuard]
},
{
path : '**',
redirectTo: 'inbox'
}
];
@NgModule({
declarations : [
FuseMailNgrxComponent,
FuseMailNgrxListComponent,
FuseMailNgrxListItemComponent,
FuseMailNgrxDetailsComponent,
FuseMailNgrxMainSidenavComponent,
FuseMailNgrxComposeDialogComponent
],
imports : [
SharedModule,
RouterModule.forChild(routes),
MailAppStoreModule
],
providers : [
MailNgrxService,
fromGuards.ResolveGuard
],
entryComponents: [FuseMailNgrxComposeDialogComponent]
})
export class FuseMailNgrxModule
{
}

View File

@@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { Mail } from './mail.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Store } from '@ngrx/store';
import { MailAppState } from './store/reducers';
import { getFiltersArr, getFoldersArr, getLabelsArr, getMailsArr } from './store/selectors';
@Injectable()
export class MailNgrxService
{
foldersArr: any;
filtersArr: any;
labelsArr: any;
selectedMails: Mail[];
mails: Mail[];
constructor(
private http: HttpClient,
private store: Store<MailAppState>
)
{
this.store.select(getFoldersArr).subscribe(folders => {
this.foldersArr = folders;
});
this.store.select(getFiltersArr).subscribe(filters => {
this.filtersArr = filters;
});
this.store.select(getLabelsArr).subscribe(labels => {
this.labelsArr = labels;
});
this.store.select(getMailsArr).subscribe(mails => {
this.mails = mails;
});
this.selectedMails = [];
}
getAllMails(): Observable<Mail[]>
{
return this.http.get<Mail[]>('api/mail-mails');
}
getFolders(): Observable<any>
{
return this.http.get('api/mail-folders');
}
getFilters(): Observable<any>
{
return this.http.get('api/mail-filters');
}
getLabels(): Observable<any>
{
return this.http.get('api/mail-labels');
}
getMails(handle): Observable<Mail[]>
{
if ( handle.id === 'labelHandle' )
{
const labelId = this.labelsArr.find(label => label.handle === handle.value).id;
return this.http.get<Mail[]>('api/mail-mails?labels=' + labelId);
}
else if ( handle.id === 'filterHandle' )
{
return this.http.get<Mail[]>('api/mail-mails?' + handle.value + '=true');
}
else // folderHandle
{
const folderId = this.foldersArr.find(folder => folder.handle === handle.value).id;
return this.http.get<any>('api/mail-mails?folder=' + folderId);
}
}
/**
* Update the mail
* @param mail
* @returns {Promise<any>}
*/
updateMail(mail)
{
return this.http.post('api/mail-mails/' + mail.id, {...mail});
}
}

View File

@@ -0,0 +1,69 @@
<!-- SIDENAV HEADER -->
<div fxLayout="column" fxLayoutAlign="space-between start"
class="header p-24 pb-4" ngClass="mat-accent-bg" ngClass.gt-md="white-fg">
<div class="logo" fxFlex fxLayout="row" fxLayoutAlign="start center">
<mat-icon class="logo-icon s-32">mail</mat-icon>
<span class="logo-text">Mailbox Ngrx</span>
</div>
<div class="account" fxLayout="column">
<div class="title">John Doe</div>
<mat-form-field floatPlaceholder="never">
<mat-select class="account-selection" placeholder="Mail Selection"
[ngModel]="selectedAccount">
<mat-option *ngFor="let account of (accounts | keys)" [value]="account.key">
{{account.value}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<!-- / SIDENAV HEADER -->
<!-- SIDENAV CONTENT -->
<div class="content" fusePerfectScrollbar>
<div class="p-24">
<button mat-raised-button fxFlex
class="mat-accent compose-dialog-button"
(click)="composeDialog()"
aria-label="Compose">
{{ 'MAIL.COMPOSE' | translate }}
</button>
</div>
<div class="nav">
<div class="nav-subheader">{{ 'MAIL.FOLDERS' | translate }}</div>
<div class="nav-item" *ngFor="let folder of (folders$ | async)">
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/' + folder.handle" routerLinkActive="active">
<mat-icon class="nav-link-icon" *ngIf="folder.icon">{{folder.icon}}</mat-icon>
<span>{{folder.title}}</span>
</a>
</div>
<div class="nav-subheader">{{ 'MAIL.FILTERS' | translate }}</div>
<div class="nav-item" *ngFor="let filter of (filters$ | async)">
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/filter/' + filter.handle" routerLinkActive="active">
<mat-icon class="nav-link-icon" *ngIf="filter.icon">{{filter.icon}}</mat-icon>
<span>{{filter.title}}</span>
</a>
</div>
<div class="nav-subheader">{{ 'MAIL.LABELS' | translate }}</div>
<div class="nav-item" *ngFor="let label of (labels$ | async)">
<a class="nav-link" matRipple [routerLink]="'/apps/mail-ngrx/label/' + label.handle" routerLinkActive="active">
<mat-icon class="nav-link-icon" [ngStyle]="{'color':label.color}">label</mat-icon>
<span>{{label.title}}</span>
</a>
</div>
</div>
</div>
<!-- / SIDENAV CONTENT -->

View File

@@ -0,0 +1,30 @@
:host {
display: flex;
flex: 1 0 auto;
flex-direction: column;
height: 100%;
.header {
.logo {
.logo-icon {
margin: 0 16px 0 0;
}
.logo-text {
font-size: 24px;
line-height: 24px;
}
}
.account {
width: 100%;
}
}
.content {
.compose-dialog-button {
}
}
}

View File

@@ -0,0 +1,84 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { FuseMailNgrxComposeDialogComponent } from '../../dialogs/compose/compose.component';
import { MatDialog } from '@angular/material';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import * as fromStore from './../../store';
import { MailNgrxService } from '../../mail.service';
@Component({
selector : 'fuse-mail-main-sidenav',
templateUrl : './main-sidenav.component.html',
styleUrls : ['./main-sidenav.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FuseMailNgrxMainSidenavComponent implements OnInit, OnDestroy
{
labels: any[];
accounts: object;
selectedAccount: string;
dialogRef: any;
folders$: Observable<any>;
filters$: Observable<any>;
labels$: Observable<any>;
constructor(
private mailService: MailNgrxService,
public dialog: MatDialog,
private store: Store<fromStore.MailAppState>
)
{
// Data
this.accounts = {
'creapond' : 'johndoe@creapond.com',
'withinpixels': 'johndoe@withinpixels.com'
};
this.selectedAccount = 'creapond';
this.folders$ = this.store.select(fromStore.getFoldersArr);
this.filters$ = this.store.select(fromStore.getFiltersArr);
this.labels$ = this.store.select(fromStore.getLabelsArr);
}
ngOnInit()
{
}
composeDialog()
{
this.dialogRef = this.dialog.open(FuseMailNgrxComposeDialogComponent, {
panelClass: 'mail-compose-dialog'
});
this.dialogRef.afterClosed()
.subscribe(response => {
if ( !response )
{
return;
}
const actionType: string = response[0];
const formData: FormGroup = response[1];
switch ( actionType )
{
/**
* Send
*/
case 'send':
console.log('new Mail', formData.getRawValue());
break;
/**
* Delete
*/
case 'delete':
console.log('delete Mail');
break;
}
});
}
ngOnDestroy()
{
}
}

View File

@@ -0,0 +1,46 @@
import { Action } from '@ngrx/store';
export const GET_FILTERS = '[FILTERS] GET FILTERS';
export const GET_FILTERS_SUCCESS = '[FILTERS] GET FILTERS SUCCESS';
export const GET_FILTERS_FAILED = '[FILTERS] GET FILTERS FAILED';
/**
* Get Filters
*/
export class GetFilters implements Action
{
readonly type = GET_FILTERS;
constructor(public payload: any)
{
}
}
/**
* Get Filters Success
*/
export class GetFiltersSuccess implements Action
{
readonly type = GET_FILTERS_SUCCESS;
constructor(public payload: any)
{
}
}
/**
* Get Filters Failed
*/
export class GetFiltersFailed implements Action
{
readonly type = GET_FILTERS_FAILED;
constructor(public payload: string)
{
}
}
export type FiltersActionsAll
= GetFilters
| GetFiltersSuccess
| GetFiltersFailed;

View File

@@ -0,0 +1,46 @@
import { Action } from '@ngrx/store';
export const GET_FOLDERS = '[FOLDERS] GET FOLDERS';
export const GET_FOLDERS_SUCCESS = '[FOLDERS] GET FOLDERS SUCCESS';
export const GET_FOLDERS_FAILED = '[FOLDERS] GET FOLDERS FAILED';
/**
* Get Folders
*/
export class GetFolders implements Action
{
readonly type = GET_FOLDERS;
constructor(public payload: any)
{
}
}
/**
* Get Folders Success
*/
export class GetFoldersSuccess implements Action
{
readonly type = GET_FOLDERS_SUCCESS;
constructor(public payload: any)
{
}
}
/**
* Get Folders Failed
*/
export class GetFoldersFailed implements Action
{
readonly type = GET_FOLDERS_FAILED;
constructor(public payload: string)
{
}
}
export type FoldersActionsAll
= GetFolders
| GetFoldersSuccess
| GetFoldersFailed;

View File

@@ -0,0 +1,4 @@
export * from './mails.actions';
export * from './folders.actions';
export * from './filters.actions';
export * from './labels.actions';

View File

@@ -0,0 +1,46 @@
import { Action } from '@ngrx/store';
export const GET_LABELS = '[LABELS] GET LABELS';
export const GET_LABELS_SUCCESS = '[LABELS] GET LABELS SUCCESS';
export const GET_LABELS_FAILED = '[LABELS] GET LABELS FAILED';
/**
* Get Labels
*/
export class GetLabels implements Action
{
readonly type = GET_LABELS;
constructor(public payload: any)
{
}
}
/**
* Get Labels Success
*/
export class GetLabelsSuccess implements Action
{
readonly type = GET_LABELS_SUCCESS;
constructor(public payload: any)
{
}
}
/**
* Get Labels Failed
*/
export class GetLabelsFailed implements Action
{
readonly type = GET_LABELS_FAILED;
constructor(public payload: string)
{
}
}
export type LabelsActionsAll
= GetLabels
| GetLabelsSuccess
| GetLabelsFailed;

View File

@@ -0,0 +1,243 @@
import { Action } from '@ngrx/store';
import { Mail } from '../../mail.model';
export const GET_MAILS = '[MAILS] GET MAILS';
export const GET_MAILS_SUCCESS = '[MAILS] GET MAILS SUCCESS';
export const GET_MAILS_FAILED = '[MAILS] GET MAILS FAILED';
export const SET_CURRENT_MAIL = '[MAILS] SET CURRENT MAIL';
export const SET_CURRENT_MAIL_SUCCESS = '[MAILS] SET CURRENT MAIL SUCCESS';
export const CHECK_CURRENT_MAIL = '[MAILS] CHECK CURRENT MAIL';
export const UPDATE_MAIL = '[MAILS] UPDATE MAIL';
export const UPDATE_MAIL_SUCCESS = '[MAILS] UPDATE MAIL SUCCESS';
export const UPDATE_MAILS = '[MAILS] UPDATE MAILS';
export const UPDATE_MAILS_SUCCESS = '[MAILS] UPDATE MAILS SUCCESS';
export const SET_SEARCH_TEXT = '[MAILS] SET SEARCH TEXT';
export const SELECT_ALL_MAILS = '[MAILS] SELECT ALL MAILS';
export const DESELECT_ALL_MAILS = '[MAILS] DESELECT ALL MAILS';
export const TOGGLE_IN_SELECTED_MAILS = '[MAILS] TOGGLE IN SELECTED MAILS';
export const SELECT_MAILS_BY_PARAMETER = '[MAILS] SELECT MAILS BY PARAMETER';
export const SET_FOLDER_ON_SELECTED_MAILS = '[MAILS] SET FOLDER ON SELECTED MAILS';
export const ADD_LABEL_ON_SELECTED_MAILS = '[MAILS] ADD LABEL ON SELECTED MAILS';
/**
* Get Mails
*/
export class GetMails implements Action
{
readonly type = GET_MAILS;
constructor()
{
}
}
/**
* Get Mails Success
*/
export class GetMailsSuccess implements Action
{
readonly type = GET_MAILS_SUCCESS;
constructor(public payload: any)
{
}
}
/**
* Get Mails Failed
*/
export class GetMailsFailed implements Action
{
readonly type = GET_MAILS_FAILED;
constructor(public payload: string)
{
}
}
/**
* Set Current Mail
*/
export class SetCurrentMail implements Action
{
readonly type = SET_CURRENT_MAIL;
constructor(public payload: string)
{
}
}
/**
* Set Current Mail Success
*/
export class SetCurrentMailSuccess implements Action
{
readonly type = SET_CURRENT_MAIL_SUCCESS;
constructor(public payload: any)
{
}
}
/**
* Check Current Mail
*/
export class CheckCurrentMail implements Action
{
readonly type = CHECK_CURRENT_MAIL;
constructor()
{
}
}
/**
* Update Mail
*/
export class UpdateMail implements Action
{
readonly type = UPDATE_MAIL;
constructor(public payload: any)
{
}
}
/**
* Update Mail Success
*/
export class UpdateMailSuccess implements Action
{
readonly type = UPDATE_MAIL_SUCCESS;
constructor(public payload: Mail)
{
}
}
/**
* Update Mails
*/
export class UpdateMails implements Action
{
readonly type = UPDATE_MAILS;
constructor(public payload: Mail[])
{
}
}
/**
* Update Mails Success
*/
export class UpdateMailsSuccess implements Action
{
readonly type = UPDATE_MAILS_SUCCESS;
constructor()
{
}
}
/**
* Set Search Text
*/
export class SetSearchText implements Action
{
readonly type = SET_SEARCH_TEXT;
constructor(public payload: string)
{
}
}
/**
* Select All Mails
*/
export class SelectAllMails implements Action
{
readonly type = SELECT_ALL_MAILS;
constructor()
{
}
}
/**
* Deselect All Mails
*/
export class DeselectAllMails implements Action
{
readonly type = DESELECT_ALL_MAILS;
constructor()
{
}
}
/**
* Toggle In Selected Mails
*/
export class ToggleInSelectedMails implements Action
{
readonly type = TOGGLE_IN_SELECTED_MAILS;
constructor(public payload: string)
{
}
}
/**
* Select Mails by Parameter
*/
export class SelectMailsByParameter implements Action
{
readonly type = SELECT_MAILS_BY_PARAMETER;
constructor(public payload: any)
{
}
}
/**
* Set Folder on Selected Mails
*/
export class SetFolderOnSelectedMails implements Action
{
readonly type = SET_FOLDER_ON_SELECTED_MAILS;
constructor(public payload: string)
{
}
}
/**
* Add label on Selected Mails
*/
export class AddLabelOnSelectedMails implements Action
{
readonly type = ADD_LABEL_ON_SELECTED_MAILS;
constructor(public payload: string)
{
}
}
export type MailsActionsAll
= GetMails
| GetMailsSuccess
| GetMailsFailed
| SetCurrentMail
| SetCurrentMailSuccess
| CheckCurrentMail
| UpdateMail
| UpdateMailSuccess
| UpdateMails
| UpdateMailsSuccess
| SetSearchText
| SelectAllMails
| DeselectAllMails
| ToggleInSelectedMails
| SelectMailsByParameter
| SetFolderOnSelectedMails
| AddLabelOnSelectedMails;

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import { of } from 'rxjs/observable/of';
import { catchError, map, switchMap } from 'rxjs/operators';
import * as FiltersActions from '../actions/filters.actions';
import { MailNgrxService } from '../../mail.service';
@Injectable()
export class FiltersEffect
{
constructor(
private actions: Actions,
private mailService: MailNgrxService
)
{
}
/**
* Get filters from Server
* @type {Observable<any>}
*/
@Effect()
getFilters: Observable<FiltersActions.FiltersActionsAll> =
this.actions
.ofType<FiltersActions.GetFilters>(FiltersActions.GET_FILTERS)
.pipe(
switchMap((action) => {
return this.mailService.getFilters()
.pipe(
map((filters: any) => {
return new FiltersActions.GetFiltersSuccess(filters);
}),
catchError(err => of(new FiltersActions.GetFiltersFailed(err)))
);
}
));
}

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import { of } from 'rxjs/observable/of';
import { catchError, map, switchMap } from 'rxjs/operators';
import * as FoldersActions from '../actions/folders.actions';
import { MailNgrxService } from '../../mail.service';
@Injectable()
export class FoldersEffect
{
constructor(
private actions: Actions,
private mailService: MailNgrxService
)
{
}
/**
* Get Folders from Server
* @type {Observable<any>}
*/
@Effect()
getFolders: Observable<FoldersActions.FoldersActionsAll> =
this.actions
.ofType<FoldersActions.GetFolders>(FoldersActions.GET_FOLDERS)
.pipe(
switchMap((action) => {
return this.mailService.getFolders()
.pipe(
map((folders: any) => {
return new FoldersActions.GetFoldersSuccess(folders);
}),
catchError(err => of(new FoldersActions.GetFoldersFailed(err)))
);
}
));
}

View File

@@ -0,0 +1,16 @@
import { MailsEffect } from './mails.effects';
import { FoldersEffect } from './folders.effects';
import { FiltersEffect } from './filters.effects';
import { LabelsEffect } from './labels.effects';
export const effects = [
MailsEffect,
FoldersEffect,
FiltersEffect,
LabelsEffect
];
export * from './mails.effects';
export * from './folders.effects';
export * from './filters.effects';
export * from './labels.effects';

View File

@@ -0,0 +1,40 @@
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import { of } from 'rxjs/observable/of';
import { catchError, map, switchMap } from 'rxjs/operators';
import * as LabelsActions from '../actions/labels.actions';
import { MailNgrxService } from '../../mail.service';
@Injectable()
export class LabelsEffect
{
constructor(
private actions: Actions,
private mailService: MailNgrxService
)
{
}
/**
* Get Labels from Server
* @type {Observable<any>}
*/
@Effect()
getLabels: Observable<LabelsActions.LabelsActionsAll> =
this.actions
.ofType<LabelsActions.GetLabels>(LabelsActions.GET_LABELS)
.pipe(
switchMap((action) => {
return this.mailService.getLabels()
.pipe(
map((labels: any) => {
return new LabelsActions.GetLabelsSuccess(labels);
}),
catchError(err => of(new LabelsActions.GetLabelsFailed(err)))
);
}
));
}

View File

@@ -0,0 +1,254 @@
import { Injectable } from '@angular/core';
import { Action, Store } from '@ngrx/store';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { map, mergeMap, exhaustMap, withLatestFrom } from 'rxjs/operators';
import { getRouterState, State } from '../../../../../../store/reducers';
import { getMailsState } from '../selectors';
import * as MailsActions from '../actions/mails.actions';
import * as fromRoot from '../../../../../../store';
import { MailNgrxService } from '../../mail.service';
import { Mail } from '../../mail.model';
@Injectable()
export class MailsEffect
{
routerState: any;
constructor(
private actions: Actions,
private mailService: MailNgrxService,
private store: Store<State>
)
{
this.store.select(getRouterState).subscribe(routerState => {
if ( routerState )
{
this.routerState = routerState.state;
}
});
}
/**
* Get Mails with router parameters
* @type {Observable<any>}
*/
@Effect()
getMails: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.GetMails>(MailsActions.GET_MAILS)
.pipe(
exhaustMap((action) => {
let handle = {
id : '',
value: ''
};
const routeParams = Observable.of('labelHandle', 'filterHandle', 'folderHandle');
routeParams.subscribe(param => {
if ( this.routerState.params[param] )
{
handle = {
id : param,
value: this.routerState.params[param]
};
}
});
return this.mailService.getMails(handle)
.map((mails: Mail[]) => {
return new MailsActions.GetMailsSuccess({
loaded: handle,
mails : mails
});
})
.catch(err => of(new MailsActions.GetMailsFailed(err)));
})
);
/**
* Update Mail
* @type {Observable<any>}
*/
@Effect()
updateMail: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.UpdateMail>(MailsActions.UPDATE_MAIL)
.pipe(
exhaustMap((action) => {
return this.mailService.updateMail(action.payload)
.map(() => {
return new MailsActions.UpdateMailSuccess(action.payload);
});
})
);
/**
* UpdateMails
* @type {Observable<any>}
*/
@Effect()
updateMails: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.UpdateMails>(MailsActions.UPDATE_MAILS)
.pipe(
exhaustMap((action) => {
return Observable.forkJoin(
action.payload.map(mail => this.mailService.updateMail(mail)),
() => {
return new MailsActions.UpdateMailsSuccess();
});
})
);
/**
* Set Current Mail
* @type {Observable<SetCurrentMailSuccess>}
*/
@Effect()
setCurrentMail: Observable<Action> =
this.actions
.ofType<MailsActions.SetCurrentMail>(MailsActions.SET_CURRENT_MAIL)
.pipe(
withLatestFrom(this.store.select(getMailsState)),
map(([action, state]) => {
return new MailsActions.SetCurrentMailSuccess(state.entities[action.payload]);
})
);
/**
* Check Current Mail
* Navigate to parent directory if not exist in mail list
* Update Current Mail if exist in mail list
* @type {Observable<any>}
*/
@Effect()
checkCurrentMail: Observable<Action> =
this.actions
.ofType<MailsActions.CheckCurrentMail>(MailsActions.CHECK_CURRENT_MAIL)
.pipe(
withLatestFrom(this.store.select(getMailsState)),
map(([action, state]) => {
if ( !state.entities[this.routerState.params.mailId] )
{
return new fromRoot.Go({path: [this.routerState.url.replace(this.routerState.params.mailId, '')]});
}
return new MailsActions.SetCurrentMailSuccess(state.entities[this.routerState.params.mailId]);
})
);
/**
* On Get Mails Success
* @type {Observable<CheckCurrentMail>}
*/
@Effect()
getMailsSuccess: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.GetMailsSuccess>(MailsActions.GET_MAILS_SUCCESS)
.pipe(
mergeMap(() =>
[
new MailsActions.CheckCurrentMail()
])
);
/**
* On Update Mails Success
* @type {Observable<DeselectAllMails | GetMails>}
*/
@Effect()
updateMailsSuccess: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.UpdateMailsSuccess>(MailsActions.UPDATE_MAILS_SUCCESS)
.pipe(
mergeMap(() =>
[
new MailsActions.DeselectAllMails(),
new MailsActions.GetMails()
])
);
/**
* On Update Mail Success
* @type {Observable<GetMails>}
*/
@Effect()
updateMailSuccess: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.UpdateMailSuccess>(MailsActions.UPDATE_MAIL_SUCCESS)
.debounceTime(500)
.pipe(
map(() => {
return new MailsActions.GetMails();
})
);
/**
* Set Folder on Selected Mails
* @type {Observable<UpdateMails>}
*/
@Effect()
setFolderOnSelectedMails: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.SetFolderOnSelectedMails>(MailsActions.SET_FOLDER_ON_SELECTED_MAILS)
.pipe(
withLatestFrom(
this.store.select(getMailsState)),
map(([action, state]) => {
const entities = {...state.entities};
let mailsToUpdate = [];
state.selectedMailIds
.map(id => {
mailsToUpdate = [
...mailsToUpdate,
entities[id] = {
...entities[id],
folder: action.payload
}
];
});
return new MailsActions.UpdateMails(mailsToUpdate);
})
);
/**
* Add Label on Selected Mails
* @type {Observable<UpdateMails>}
*/
@Effect()
addLabelOnSelectedMails: Observable<MailsActions.MailsActionsAll> =
this.actions
.ofType<MailsActions.AddLabelOnSelectedMails>(MailsActions.ADD_LABEL_ON_SELECTED_MAILS)
.pipe(
withLatestFrom(this.store.select(getMailsState)),
map(([action, state]) => {
const entities = {...state.entities};
let mailsToUpdate = [];
state.selectedMailIds
.map(id => {
let labels = [...entities[id].labels];
if ( !entities[id].labels.includes(action.payload) )
{
labels = [...labels, action.payload];
}
mailsToUpdate = [
...mailsToUpdate,
entities[id] = {
...entities[id],
labels
}
];
});
return new MailsActions.UpdateMails(mailsToUpdate);
})
);
}

View File

@@ -0,0 +1 @@
export * from './resolve.guard';

View File

@@ -0,0 +1,134 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { map, switchMap, catchError, tap, take, filter } from 'rxjs/operators';
import 'rxjs/add/observable/forkJoin';
import { MailAppState } from '../reducers';
import * as fromStore from '../index';
import { getFiltersLoaded, getFoldersLoaded, getLabelsLoaded, getMailsLoaded } from '../selectors';
import { RouterStateSnapshot } from '@angular/router/src/router_state';
import { getRouterState } from '../../../../../../store/reducers';
@Injectable()
export class ResolveGuard implements CanActivate
{
routerState: any;
constructor(
private store: Store<MailAppState>
)
{
this.store.select(getRouterState).subscribe(routerState => {
if ( routerState )
{
this.routerState = routerState.state;
}
});
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean>
{
return this.checkStore().pipe(
switchMap(() => of(true)),
catchError(() => of(false))
);
}
checkStore(): Observable<any>
{
return Observable
.forkJoin(
this.getFolders(),
this.getFilters(),
this.getLabels()
)
.pipe(
filter(([foldersLoaded, filtersLoaded, labelsLoaded]) => filtersLoaded && foldersLoaded && labelsLoaded),
take(1),
switchMap(() =>
this.getMails()
),
take(1),
map(() => this.store.dispatch(new fromStore.SetCurrentMail(this.routerState.params.mailId)))
);
}
getFolders()
{
return this.store.select(getFoldersLoaded)
.pipe(
tap(loaded => {
if ( !loaded )
{
this.store.dispatch(new fromStore.GetFolders([]));
}
}),
filter(loaded => loaded),
take(1)
);
}
/**
* Get Filters
* @returns {Observable<any>}
*/
getFilters()
{
return this.store.select(getFiltersLoaded)
.pipe(
tap(loaded => {
if ( !loaded )
{
this.store.dispatch(new fromStore.GetFilters([]));
}
}),
filter(loaded => loaded),
take(1)
);
}
/**
* Get Labels
* @returns {Observable<any>}
*/
getLabels()
{
return this.store.select(getLabelsLoaded)
.pipe(
tap(loaded => {
if ( !loaded )
{
this.store.dispatch(new fromStore.GetLabels([]));
}
}),
filter(loaded => loaded),
take(1)
);
}
/**
* Get Mails
* @returns {Observable<any>}
*/
getMails()
{
return this.store.select(getMailsLoaded)
.pipe(
tap((loaded: any) => {
if ( !this.routerState.params[loaded.id] || this.routerState.params[loaded.id] !== loaded.value )
{
this.store.dispatch(new fromStore.GetMails());
this.store.dispatch(new fromStore.SetSearchText(''));
this.store.dispatch(new fromStore.DeselectAllMails());
}
}),
filter((loaded: any) => {
return this.routerState.params[loaded.id] && this.routerState.params[loaded.id] === loaded.value;
}),
take(1)
);
}
}

View File

@@ -0,0 +1,4 @@
export * from './actions';
export * from './reducers';
export * from './selectors';
export * from './effects';

View File

@@ -0,0 +1,53 @@
import * as FiltersActions from '../actions/filters.actions';
export interface FiltersState
{
entities: { [id: number]: any };
loading: boolean;
loaded: boolean;
}
export const FiltersInitialState: FiltersState = {
entities: {},
loading : false,
loaded : false
};
export function FiltersReducer(state = FiltersInitialState, action: FiltersActions.FiltersActionsAll): FiltersState
{
switch ( action.type )
{
case FiltersActions.GET_FILTERS:
return {
...state,
loading: true,
loaded : false
};
case FiltersActions.GET_FILTERS_SUCCESS:
const filters = action.payload;
const entities = filters.reduce(
(_entities: { [id: number]: any }, filter: any) => {
return {
..._entities,
[filter.id]: filter
};
}, {});
return {
...state,
loading: false,
loaded : true,
entities
};
case FiltersActions.GET_FILTERS_FAILED:
return {
...state,
loading: false,
loaded : false
};
default:
return state;
}
}

View File

@@ -0,0 +1,53 @@
import * as FoldersActions from '../actions/folders.actions';
export interface FoldersState
{
entities: { [id: number]: any };
loading: boolean;
loaded: boolean;
}
export const FoldersInitialState: FoldersState = {
entities: {},
loading : false,
loaded : false
};
export function FoldersReducer(state = FoldersInitialState, action: FoldersActions.FoldersActionsAll): FoldersState
{
switch ( action.type )
{
case FoldersActions.GET_FOLDERS:
return {
...state,
loading: true,
loaded : false
};
case FoldersActions.GET_FOLDERS_SUCCESS:
const folders = action.payload;
const entities = folders.reduce(
(_entities: { [id: number]: any }, folder: any) => {
return {
..._entities,
[folder.id]: folder
};
}, {});
return {
...state,
loading: false,
loaded : true,
entities
};
case FoldersActions.GET_FOLDERS_FAILED:
return {
...state,
loading: false,
loaded : false
};
default:
return state;
}
}

View File

@@ -0,0 +1,34 @@
import { ActionReducerMap, createFeatureSelector, createSelector } from '@ngrx/store';
import { MailsReducer, MailsState } from './mails.reducer';
import { FoldersReducer, FoldersState } from './folders.reducer';
import { FiltersReducer, FiltersState } from './filters.reducer';
import { LabelsReducer, LabelsState } from './labels.reducer';
export interface MailAppState
{
mails: MailsState;
folders: FoldersState;
filters: FiltersState;
labels: LabelsState;
}
export const getMailAppState = createFeatureSelector<MailAppState>(
'mail-app'
);
export const getAppState = createSelector(
getMailAppState,
(state: MailAppState) => state
);
export const reducers: ActionReducerMap<MailAppState> = {
mails : MailsReducer,
folders: FoldersReducer,
filters: FiltersReducer,
labels : LabelsReducer
};
export * from './mails.reducer';
export * from './folders.reducer';
export * from './filters.reducer';
export * from './labels.reducer';

View File

@@ -0,0 +1,53 @@
import * as LabelsActions from '../actions/labels.actions';
export interface LabelsState
{
entities: { [id: number]: any };
loading: boolean;
loaded: boolean;
}
export const LabelsInitialState: LabelsState = {
entities: {},
loading : false,
loaded : false
};
export function LabelsReducer(state = LabelsInitialState, action: LabelsActions.LabelsActionsAll): LabelsState
{
switch ( action.type )
{
case LabelsActions.GET_LABELS:
return {
...state,
loading: true,
loaded : false
};
case LabelsActions.GET_LABELS_SUCCESS:
const labels = action.payload;
const entities = labels.reduce(
(_entities: { [id: number]: any }, label: any) => {
return {
..._entities,
[label.id]: label
};
}, {});
return {
...state,
loading: false,
loaded : true,
entities
};
case LabelsActions.GET_LABELS_FAILED:
return {
...state,
loading: false,
loaded : false
};
default:
return state;
}
}

View File

@@ -0,0 +1,167 @@
import * as MailsActions from '../actions/mails.actions';
import { Mail } from '../../mail.model';
export interface MailsState
{
entities: { [id: number]: Mail };
currentMail: any;
selectedMailIds: string[];
searchText: string;
loading: boolean;
loaded: any;
}
export const MailsInitialState: MailsState = {
entities : {},
currentMail : null,
selectedMailIds: [],
searchText : '',
loading : false,
loaded : false
};
export function MailsReducer(state = MailsInitialState, action: MailsActions.MailsActionsAll): MailsState
{
switch ( action.type )
{
case MailsActions.GET_MAILS:
{
return {
...state,
loading: true
};
}
case MailsActions.GET_MAILS_SUCCESS:
{
const mails = action.payload.mails;
const loaded = action.payload.loaded;
const entities = mails.reduce(
(_entities: { [id: number]: Mail }, mail: Mail) => {
return {
..._entities,
[mail.id]: mail
};
}, {});
return {
...state,
entities,
loading: false,
loaded
};
}
case MailsActions.GET_MAILS_FAILED:
{
return {
...state,
loading: false,
loaded : false
};
}
case MailsActions.SET_CURRENT_MAIL_SUCCESS:
{
return {
...state,
currentMail: action.payload
};
}
case MailsActions.UPDATE_MAIL_SUCCESS:
{
return {
...state,
entities: {
...state.entities,
[action.payload.id]: action.payload
}
};
}
case MailsActions.SET_SEARCH_TEXT:
{
return {
...state,
searchText: action.payload
};
}
case MailsActions.TOGGLE_IN_SELECTED_MAILS:
{
const mailId = action.payload;
let selectedMailIds = [...state.selectedMailIds];
if ( selectedMailIds.find(id => id === mailId) !== undefined )
{
selectedMailIds = selectedMailIds.filter(id => id !== mailId);
}
else
{
selectedMailIds = [...selectedMailIds, mailId];
}
return {
...state,
selectedMailIds
};
}
case MailsActions.SELECT_ALL_MAILS:
{
const arr = Object.keys(state.entities).map(k => state.entities[k]);
const selectedMailIds = arr.map(mail => mail.id);
return {
...state,
selectedMailIds
};
}
case MailsActions.DESELECT_ALL_MAILS:
{
return {
...state,
selectedMailIds: []
};
}
case MailsActions.SELECT_MAILS_BY_PARAMETER:
{
const filter = action.payload;
const arr = Object.keys(state.entities).map(k => state.entities[k]);
const selectedMailIds = arr.filter(mail => mail[filter.parameter] === filter.value)
.map(mail => mail.id);
return {
...state,
selectedMailIds
};
}
case MailsActions.SET_FOLDER_ON_SELECTED_MAILS:
{
const entities = {...state.entities};
state.selectedMailIds.map(id => {
entities[id] = {
...entities[id],
folder: action.payload
};
});
return {
...state,
entities
};
}
default:
return state;
}
}

View File

@@ -0,0 +1,22 @@
import { createSelector } from '@ngrx/store';
import { FiltersState, getMailAppState, MailAppState } from '../reducers';
export const getFiltersState = createSelector(
getMailAppState,
(state: MailAppState) => state.filters
);
export const getFilters = createSelector(
getFiltersState,
(state: FiltersState) => state.entities
);
export const getFiltersLoaded = createSelector(
getFiltersState,
(state: FiltersState) => state.loaded
);
export const getFiltersArr = createSelector(
getFilters,
(entities) => Object.keys(entities).map((id) => entities[id])
);

View File

@@ -0,0 +1,22 @@
import { createSelector } from '@ngrx/store';
import { FoldersState, getMailAppState, MailAppState } from '../reducers';
export const getFoldersState = createSelector(
getMailAppState,
(state: MailAppState) => state.folders
);
export const getFolders = createSelector(
getFoldersState,
(state: FoldersState) => state.entities
);
export const getFoldersLoaded = createSelector(
getFoldersState,
(state: FoldersState) => state.loaded
);
export const getFoldersArr = createSelector(
getFolders,
(entities) => Object.keys(entities).map((id) => entities[id])
);

View File

@@ -0,0 +1,4 @@
export * from './mails.selectors';
export * from './folders.selectors';
export * from './filters.selectors';
export * from './labels.selectors';

View File

@@ -0,0 +1,22 @@
import { createSelector } from '@ngrx/store';
import { LabelsState, getMailAppState, MailAppState } from '../reducers';
export const getLabelsState = createSelector(
getMailAppState,
(state: MailAppState) => state.labels
);
export const getLabels = createSelector(
getLabelsState,
(state: LabelsState) => state.entities
);
export const getLabelsLoaded = createSelector(
getLabelsState,
(state: LabelsState) => state.loaded
);
export const getLabelsArr = createSelector(
getLabels,
(entities) => Object.keys(entities).map((id) => entities[id])
);

View File

@@ -0,0 +1,42 @@
import { createSelector } from '@ngrx/store';
import { getMailAppState, MailAppState, MailsState } from '../reducers';
import { FuseUtils } from '../../../../../../core/fuseUtils';
export const getMailsState = createSelector(
getMailAppState,
(state: MailAppState) => state.mails
);
export const getMails = createSelector(
getMailsState,
(state: MailsState) => state.entities
);
export const getMailsLoaded = createSelector(
getMailsState,
(state: MailsState) => state.loaded
);
export const getSearchText = createSelector(
getMailsState,
(state: MailsState) => state.searchText
);
export const getMailsArr = createSelector(
getMails,
getSearchText,
(entities, searchText) => {
const arr = Object.keys(entities).map((id) => entities[id]);
return FuseUtils.filterArrayByString(arr, searchText);
}
);
export const getCurrentMail = createSelector(
getMailsState,
(state: MailsState) => state.currentMail
);
export const getSelectedMailIds = createSelector(
getMailsState,
(state: MailsState) => state.selectedMailIds
);

View File

@@ -0,0 +1,16 @@
import { StoreModule } from '@ngrx/store';
import { NgModule } from '@angular/core';
import { EffectsModule } from '@ngrx/effects';
import { reducers } from './reducers';
import { effects } from './effects';
@NgModule({
imports : [
StoreModule.forFeature('mail-app', reducers),
EffectsModule.forFeature(effects)
],
providers: []
})
export class MailAppStoreModule
{
}

View File

@@ -28,7 +28,12 @@
type="email" required>
</mat-form-field>
<mat-form-field>
<div class="show-hide-extra-fields" (click)="toggleExtraToFields()">
<span [fxShow]="!showExtraToFields">Show CC & BCC</span>
<span [fxShow]="showExtraToFields">Hide CC & BCC</span>
</div>
<mat-form-field [fxShow]="showExtraToFields">
<input matInput
name="cc"
placeholder="Cc"
@@ -36,7 +41,7 @@
type="email">
</mat-form-field>
<mat-form-field>
<mat-form-field [fxShow]="showExtraToFields">
<input matInput
name="bcc"
placeholder="Bcc"
@@ -51,11 +56,11 @@
</mat-form-field>
<mat-form-field>
<textarea matInput name="message"
placeholder="Message"
formControlName="message"
rows="6">
</textarea>
<textarea matInput name="message"
placeholder="Message"
formControlName="message"
rows="6">
</textarea>
</mat-form-field>
<div class="attachment-list">

View File

@@ -1,7 +1,17 @@
@import "src/app/core/scss/fuse";
.mail-compose-dialog {
@include media-breakpoint('xs') {
width: 100%;
}
@include media-breakpoint-up('sm') {
width: 640px;
}
.mat-dialog-container {
padding: 0;
width: 720px;
.compose-form {
@@ -9,6 +19,12 @@
width: 100%;
}
.show-hide-extra-fields {
text-align: right;
cursor: pointer;
font-size: 12px;
}
.attachment-list {
font-size: 13px;
padding-top: 16px;

View File

@@ -10,6 +10,7 @@ import { FormControl, FormGroup } from '@angular/forms';
})
export class FuseMailComposeDialogComponent implements OnInit
{
showExtraToFields = false;
composeForm: FormGroup;
constructor(
@@ -39,4 +40,8 @@ export class FuseMailComposeDialogComponent implements OnInit
});
}
toggleExtraToFields()
{
this.showExtraToFields = !this.showExtraToFields;
}
}

View File

@@ -8,7 +8,7 @@ export const locale = {
'LABELS' : 'LABELS',
'NO_MESSAGES' : 'There are no messages!',
'SELECT_A_MESSAGE_TO_READ': 'Select a message to read',
'SEARCH_PLACEHOLDER': 'Search for an e-mail or contact'
'SEARCH_PLACEHOLDER' : 'Search for an e-mail or contact'
}
}
};

View File

@@ -85,7 +85,7 @@
</mat-menu>
</div>
<div *ngIf="currentMail" fxHide.gt-lg>
<div *ngIf="currentMail" fxHide.gt-xs>
<button mat-icon-button (click)="deSelectCurrentMail()">
<mat-icon>arrow_back</mat-icon>
</button>

View File

@@ -4,6 +4,8 @@ import { Subscription } from 'rxjs/Subscription';
import { FormControl } from '@angular/forms';
import { Mail } from './mail.model';
import { FuseTranslationLoaderService } from '../../../../core/services/translation-loader.service';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { locale as english } from './i18n/en';
import { locale as turkish } from './i18n/tr';
@@ -80,12 +82,12 @@ export class FuseMailComponent implements OnInit, OnDestroy
}
});
/*this.searchInput.valueChanges
this.searchInput.valueChanges
.debounceTime(300)
.distinctUntilChanged()
.subscribe(searchText => {
this.mailService.onSearchTextChanged.next(searchText);
});*/
});
}
ngOnDestroy()

View File

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { Mail } from './mail.model';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { FuseUtils } from 'app/core/fuseUtils';
import { FuseUtils } from '../../../../core/fuseUtils';
@Injectable()
export class MailService implements Resolve<any>

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