first commit

This commit is contained in:
2024-05-16 16:05:41 +02:00
commit adf8283ae0
197 changed files with 26666 additions and 0 deletions
@@ -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) {
}
}
@@ -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);
}
}
@@ -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>
@@ -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';
}
}
+42
View File
@@ -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;
}