first commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<h1 mat-dialog-title>
|
||||
{{title}}
|
||||
</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<p>{{message}}</p>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onDismiss()">No</button>
|
||||
<button mat-raised-button color="primary" (click)="onConfirm()">Yes</button>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-dialog',
|
||||
templateUrl: './confirm-dialog.component.html',
|
||||
styleUrls: ['./confirm-dialog.component.css']
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ConfirmDialogModel) {
|
||||
this.title = data.title;
|
||||
this.message = data.message;
|
||||
}
|
||||
|
||||
onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
onDismiss(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to represent confirm dialog model.
|
||||
*
|
||||
* It has been kept here to keep it as part of shared component.
|
||||
*/
|
||||
export class ConfirmDialogModel {
|
||||
|
||||
constructor(public title: string, public message: string) {
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
.container {
|
||||
height: 220px;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 12px;
|
||||
margin: 10px;
|
||||
animation: pulse 1s infinite ease-in-out;
|
||||
-webkit-animation: pulse 1s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.container div:nth-child(1) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.container div:nth-child(2) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.container div:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.container div:nth-child(4) {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.container div:nth-child(5) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.container div:nth-child(6) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.container div:nth-child(7) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.container div:nth-child(8) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
.container div:nth-child(9) {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.container div:nth-child(10) {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
background-color: rgba(165, 165, 165, .1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(165, 165, 165, .3);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(165, 165, 165, .1);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
background-color: rgba(165, 165, 165, .1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(165, 165, 165, .3);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(165, 165, 165, .1);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content-placeholder-animation',
|
||||
templateUrl: './content-placeholder-animation.component.html',
|
||||
styleUrls: ['./content-placeholder-animation.component.css']
|
||||
})
|
||||
export class ContentPlaceholderAnimationComponent {
|
||||
|
||||
constructor() { }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.navbar-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.navbar-is-mobile .navbar {
|
||||
position: fixed;
|
||||
/* Make sure the toolbar will stay on top of the content as it scrolls past. */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.navbar-sidenav-container {
|
||||
/* When the sidenav is not fixed, stretch the sidenav container to fill the available space. This
|
||||
causes `<mat-sidenav-content>` to act as our scrolling element for desktop layouts. */
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.navbar-is-mobile .navbar-sidenav-container {
|
||||
/* When the sidenav is fixed, don't constrain the height of the sidenav container. This allows the
|
||||
`<body>` to be our scrolling element for mobile layouts. */
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
/*Set sidenav width*/
|
||||
|
||||
mat-sidenav {
|
||||
min-width: 180px !important;
|
||||
border-right: 1px solid #eee;
|
||||
box-shadow: 6px 0 6px rgba(0, 0, 0, .1);
|
||||
/* background-color:rgb(63, 81, 181); */
|
||||
}
|
||||
|
||||
/* Set height of wrapper to stop content from moving up & down */
|
||||
|
||||
.progress-bar-container {
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
a.mat-list-item.active {
|
||||
background: rgba(0, 0, 0, .04);
|
||||
}
|
||||
|
||||
#push-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<div class="navbar-container" [class.example-is-mobile]="mobileQuery.matches">
|
||||
|
||||
<!-- Top navbar -->
|
||||
<mat-toolbar class="navbar" color="primary">
|
||||
|
||||
<button mat-icon-button (click)="snav.toggle()">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" [routerLink]="['/']" matTooltip="Home">
|
||||
<h1>
|
||||
Jucundus
|
||||
</h1>
|
||||
</a>
|
||||
|
||||
<span class="navbar-spacer"></span>
|
||||
|
||||
<button mat-icon-button [matMenuTriggerFor]="notificationMenu">
|
||||
<mat-icon matBadge="2" matBadgeColor="accent">notifications</mat-icon>
|
||||
</button>
|
||||
<mat-menu #notificationMenu="matMenu" xPosition="before" yPosition="above" [overlapTrigger]="false">
|
||||
|
||||
<a mat-menu-item>
|
||||
<span>You have new tasks</span>
|
||||
</a>
|
||||
<a mat-menu-item>
|
||||
<span>You have a new message</span>
|
||||
</a>
|
||||
|
||||
</mat-menu>
|
||||
|
||||
<button mat-button [matMenuTriggerFor]="userMenu">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span fxShow fxHide.xs>
|
||||
{{userName}}
|
||||
</span>
|
||||
</button>
|
||||
<mat-menu #userMenu="matMenu" xPosition="before" yPosition="above" [overlapTrigger]="false">
|
||||
|
||||
<a mat-menu-item [routerLink]="['/account/profile']">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>Account</span>
|
||||
</a>
|
||||
|
||||
<a mat-menu-item [routerLink]="['/auth/login']">
|
||||
<mat-icon>exit_to_app</mat-icon>
|
||||
<span>Log out</span>
|
||||
</a>
|
||||
|
||||
</mat-menu>
|
||||
|
||||
</mat-toolbar>
|
||||
|
||||
<mat-sidenav-container class="navbar-sidenav-container">
|
||||
<!-- Side nav -->
|
||||
<mat-sidenav #snav [opened]="!mobileQuery.matches" [mode]="mobileQuery.matches ? 'over' : 'side'"
|
||||
[fixedInViewport]="mobileQuery.matches" fixedTopGap="56" class="sidenav">
|
||||
|
||||
<mat-nav-list>
|
||||
<h3 mat-subheader>Home</h3>
|
||||
|
||||
<a mat-list-item [routerLink]="['/dashboard']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
dashboard
|
||||
</mat-icon>
|
||||
<p mat-line> Dashboard </p>
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="['/sales']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
today
|
||||
</mat-icon>
|
||||
<p mat-line> Sales </p>
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="['/favorites']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
bookmark
|
||||
</mat-icon>
|
||||
<p mat-line> Favorites </p>
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="['/pictures']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
image
|
||||
</mat-icon>
|
||||
<p mat-line> Pictures </p>
|
||||
</a>
|
||||
|
||||
|
||||
<!-- <a mat-list-item [routerLink]="['/customers']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
business
|
||||
</mat-icon>
|
||||
<p mat-line> Customers </p>
|
||||
</a>
|
||||
<a mat-list-item [routerLink]="['/users']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
people
|
||||
</mat-icon>
|
||||
<p mat-line> Users </p>
|
||||
</a> -->
|
||||
|
||||
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
<h3 mat-subheader>User</h3>
|
||||
<a mat-list-item [routerLink]="['/account/profile']">
|
||||
<mat-icon matListItemIcon>person</mat-icon>
|
||||
<p mat-line> Account </p>
|
||||
</a>
|
||||
|
||||
<a mat-list-item [routerLink]="['/auth/login']">
|
||||
<mat-icon matListItemIcon>exit_to_app</mat-icon>
|
||||
<p mat-line> Log out </p>
|
||||
</a>
|
||||
|
||||
<a id="push-bottom" mat-list-item [routerLink]="['/about']" routerLinkActive="active">
|
||||
<mat-icon matListItemIcon>
|
||||
info_outline
|
||||
</mat-icon>
|
||||
<p mat-line> About </p>
|
||||
</a>
|
||||
</mat-nav-list>
|
||||
|
||||
</mat-sidenav>
|
||||
|
||||
<!-- Main content -->
|
||||
<mat-sidenav-content class="sidenav-content">
|
||||
|
||||
<div class="progress-bar-container">
|
||||
<mat-progress-bar color="accent" mode="indeterminate" *ngIf="spinnerService.visibility | async">
|
||||
</mat-progress-bar>
|
||||
<!-- <mat-spinner class="spinner" [diameter]="50" *ngIf="showSpinner"></mat-spinner> -->
|
||||
</div>
|
||||
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Component, OnInit, ChangeDetectorRef, OnDestroy, AfterViewInit } from '@angular/core';
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { timer } from 'rxjs';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
import { SpinnerService } from '../../core/services/spinner.service';
|
||||
import { AuthGuard } from 'src/app/core/guards/auth.guard';
|
||||
|
||||
@Component({
|
||||
selector: 'app-layout',
|
||||
templateUrl: './layout.component.html',
|
||||
styleUrls: ['./layout.component.css']
|
||||
})
|
||||
export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
private _mobileQueryListener: () => void;
|
||||
mobileQuery: MediaQueryList;
|
||||
showSpinner: boolean = false;
|
||||
userName: string = "";
|
||||
isAdmin: boolean = false;
|
||||
|
||||
private autoLogoutSubscription: Subscription = new Subscription;
|
||||
|
||||
constructor(private changeDetectorRef: ChangeDetectorRef,
|
||||
private media: MediaMatcher,
|
||||
public spinnerService: SpinnerService,
|
||||
private authService: AuthenticationService,
|
||||
private authGuard: AuthGuard) {
|
||||
|
||||
this.mobileQuery = this.media.matchMedia('(max-width: 1000px)');
|
||||
this._mobileQueryListener = () => changeDetectorRef.detectChanges();
|
||||
// tslint:disable-next-line: deprecation
|
||||
this.mobileQuery.addListener(this._mobileQueryListener);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const user = this.authService.getCurrentUser();
|
||||
|
||||
this.isAdmin = user.isAdmin;
|
||||
this.userName = user.fullName;
|
||||
|
||||
// Auto log-out subscription
|
||||
const timer$ = timer(2000, 5000);
|
||||
this.autoLogoutSubscription = timer$.subscribe(() => {
|
||||
this.authGuard.canActivate();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// tslint:disable-next-line: deprecation
|
||||
this.mobileQuery.removeListener(this._mobileQueryListener);
|
||||
this.autoLogoutSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.changeDetectorRef.detectChanges();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SpinnerService } from '../../core/services/spinner.service';
|
||||
|
||||
export class SpinnerConsumer {
|
||||
isBusy = false;
|
||||
|
||||
constructor(private spinnerService: SpinnerService) {
|
||||
this.spinnerService.visibility.subscribe((value: boolean) => {
|
||||
this.isBusy = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { LimitToPipe } from './limit-to.pipe';
|
||||
|
||||
describe('LimitToPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns same value when length is shorter than specified', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform('some text', '20');
|
||||
expect(result).toBe('some text');
|
||||
});
|
||||
|
||||
it('returns limited value when length is longer than specified', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform('some text', '3');
|
||||
expect(result).toBe('som..');
|
||||
});
|
||||
|
||||
it('returns empty string when value is empty', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform('', '3');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when value is null', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform(null, '3');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when value is undefined', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform(undefined, '3');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when limit to is 0', () => {
|
||||
const pipe = new LimitToPipe();
|
||||
const result = pipe.transform(undefined, '0');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'limitTo'
|
||||
})
|
||||
export class LimitToPipe implements PipeTransform {
|
||||
|
||||
transform(value: string, limitTo: string): string {
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const limit = limitTo ? parseInt(limitTo, 10) : 10;
|
||||
const trail = '..';
|
||||
|
||||
return value.length > limit ? value.substring(0, limit) + trail : value;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LocalDatePipe } from './local-date.pipe';
|
||||
|
||||
describe('LocalDatePipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new LocalDatePipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns valid date given utc', () => {
|
||||
const date = new Date('2018-05-04T08:17:57.8979116Z');
|
||||
|
||||
const pipe = new LocalDatePipe();
|
||||
const result = pipe.transform(date, 'DD MMM YYYY HH:mm');
|
||||
expect(result).toBe('04 May 2018 09:17');
|
||||
});
|
||||
|
||||
it('returns empty string when date is null', () => {
|
||||
const date = null;
|
||||
|
||||
const pipe = new LocalDatePipe();
|
||||
const result = pipe.transform(date, 'DD MMM YYYY HH:mm');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string when format is null', () => {
|
||||
const date = new Date('2018-05-04T08:17:57.8979116Z');
|
||||
|
||||
const pipe = new LocalDatePipe();
|
||||
const result = pipe.transform(date, null);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Pipe({
|
||||
name: 'localDate'
|
||||
})
|
||||
export class LocalDatePipe implements PipeTransform {
|
||||
|
||||
transform(value: Date, args: string): string {
|
||||
if (!value || !args) {
|
||||
return '';
|
||||
}
|
||||
return moment.utc(value).local().format(args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { YesNoPipe } from './yes-no.pipe';
|
||||
|
||||
describe('YesNoPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new YesNoPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns Yes given true', () => {
|
||||
const pipe = new YesNoPipe();
|
||||
|
||||
const result = pipe.transform(true);
|
||||
|
||||
expect(result).toBe('Yes');
|
||||
});
|
||||
|
||||
it('returns No given false', () => {
|
||||
const pipe = new YesNoPipe();
|
||||
|
||||
const result = pipe.transform(false);
|
||||
|
||||
expect(result).toBe('No');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'yesNo'
|
||||
})
|
||||
export class YesNoPipe implements PipeTransform {
|
||||
|
||||
transform(value: boolean): string {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { CustomMaterialModule } from '../custom-material/custom-material.module';
|
||||
import { LimitToPipe } from './pipes/limit-to.pipe';
|
||||
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
||||
import { ContentPlaceholderAnimationComponent } from './content-placeholder-animation/content-placeholder-animation.component';
|
||||
import { LocalDatePipe } from './pipes/local-date.pipe';
|
||||
import { YesNoPipe } from './pipes/yes-no.pipe';
|
||||
import { LayoutComponent } from './layout/layout.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule,
|
||||
CustomMaterialModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FlexLayoutModule,
|
||||
],
|
||||
declarations: [
|
||||
ConfirmDialogComponent,
|
||||
ContentPlaceholderAnimationComponent,
|
||||
LimitToPipe,
|
||||
LocalDatePipe,
|
||||
YesNoPipe,
|
||||
LayoutComponent
|
||||
],
|
||||
exports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
FlexLayoutModule,
|
||||
CustomMaterialModule,
|
||||
LimitToPipe,
|
||||
ConfirmDialogComponent,
|
||||
ContentPlaceholderAnimationComponent,
|
||||
LocalDatePipe,
|
||||
YesNoPipe
|
||||
]
|
||||
})
|
||||
export class SharedModule { }
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
|
||||
export function AutocompleteSelectionValidator(control: AbstractControl) {
|
||||
const selection = control.value;
|
||||
|
||||
if (selection && typeof selection === 'string') {
|
||||
return { incorrect: true };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user