@ -0,0 +1,21 @@ | |||
MIT License | |||
Copyright (c) 2018 Angular University | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is | |||
furnished to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
SOFTWARE. |
@ -0,0 +1,67 @@ | |||
## Important Information | |||
This repository is exclusively meant for presenting samples of code. | |||
The Files and Codes present in this repository are part of a running project. In order to ensure the security of the project several files have been intentionally removed. The codes are exclusively for viewing purpose and will not execute properly if tried. | |||
# Installation pre-requisites | |||
For taking the course we recommend installing Node 12. These are some tutorials to install node in different operating systems: | |||
- [Install Node and NPM on Windows](https://www.youtube.com/watch?v=8ODS6RM6x7g) | |||
- [Install Node and NPM on Linux](https://www.youtube.com/watch?v=yUdHk-Dk_BY) | |||
- [Install Node and NPM on Mac](https://www.youtube.com/watch?v=Imj8PgG3bZU) | |||
To easily switch between node versions on your machine, we recommend using a node virtual environment tool such as [nave](https://www.npmjs.com/package/nave) or [nvm-windows](https://github.com/coreybutler/nvm-windows), depending on your operating system. | |||
For example, here is how you switch to a new node version using nave: | |||
# note that you don't even need to update your node version before installing nave | |||
npm install -g nave | |||
nave use 12.3.1 | |||
node -v | |||
v12.3.1 | |||
# Installing the Angular CLI | |||
With the following command the angular-cli will be installed globally in your machine: | |||
npm install -g @angular/cli | |||
# How To install this repository | |||
We can install the master branch using the following commands: | |||
git clone https://github.com/angular-university/ngrx-course.git | |||
This repository is made of several separate npm modules, that are installable separately. For example, to run the au-input module, we can do the following: | |||
cd ngrx-course | |||
npm install | |||
Its also possible to install the modules as usual using npm: | |||
npm install | |||
This should take a couple of minutes. If there are issues, please post the complete error message in the Questions section of the course. | |||
# To Run the Development Backend Server | |||
We can start the sample application backend with the following command: | |||
npm run server | |||
This is a small Node REST API server. | |||
# To run the Development UI Server | |||
To run the frontend part of our code, we will use the Angular CLI: | |||
npm start | |||
The application is visible at port 4200: [http://localhost:4200](http://localhost:4200) | |||
@ -0,0 +1,143 @@ | |||
{ | |||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", | |||
"version": 1, | |||
"newProjectRoot": "projects", | |||
"projects": { | |||
"angular-ngrx-course": { | |||
"root": "", | |||
"sourceRoot": "src", | |||
"projectType": "application", | |||
"architect": { | |||
"build": { | |||
"builder": "@angular-devkit/build-angular:browser", | |||
"options": { | |||
"aot": true, | |||
"outputPath": "dist", | |||
"index": "src/index.html", | |||
"main": "src/main.ts", | |||
"tsConfig": "src/tsconfig.app.json", | |||
"polyfills": "src/polyfills.ts", | |||
"assets": [ | |||
"src/assets", | |||
"src/favicon.ico" | |||
], | |||
"styles": [ | |||
"src/assets/styles/app.scss" | |||
], | |||
"scripts": [] | |||
}, | |||
"configurations": { | |||
"production": { | |||
"budgets": [ | |||
{ | |||
"type": "anyComponentStyle", | |||
"maximumWarning": "6kb" | |||
} | |||
], | |||
"optimization": true, | |||
"outputHashing": "all", | |||
"sourceMap": false, | |||
"extractCss": true, | |||
"namedChunks": false, | |||
"aot": true, | |||
"extractLicenses": true, | |||
"vendorChunk": false, | |||
"buildOptimizer": true, | |||
"fileReplacements": [ | |||
{ | |||
"replace": "src/environments/environment.ts", | |||
"with": "src/environments/environment.prod.ts" | |||
} | |||
] | |||
} | |||
} | |||
}, | |||
"serve": { | |||
"builder": "@angular-devkit/build-angular:dev-server", | |||
"options": { | |||
"browserTarget": "angular-ngrx-course:build" | |||
}, | |||
"configurations": { | |||
"production": { | |||
"browserTarget": "angular-ngrx-course:build:production" | |||
} | |||
} | |||
}, | |||
"extract-i18n": { | |||
"builder": "@angular-devkit/build-angular:extract-i18n", | |||
"options": { | |||
"browserTarget": "angular-ngrx-course:build" | |||
} | |||
}, | |||
"test": { | |||
"builder": "@angular-devkit/build-angular:karma", | |||
"options": { | |||
"main": "src/test.ts", | |||
"karmaConfig": "./karma.conf.js", | |||
"polyfills": "src/polyfills.ts", | |||
"tsConfig": "src/tsconfig.spec.json", | |||
"scripts": [], | |||
"styles": [ | |||
"src/styles.scss" | |||
], | |||
"assets": [ | |||
"src/assets", | |||
"src/favicon.ico" | |||
] | |||
} | |||
}, | |||
"lint": { | |||
"builder": "@angular-devkit/build-angular:tslint", | |||
"options": { | |||
"tsConfig": [ | |||
"src/tsconfig.app.json", | |||
"src/tsconfig.spec.json" | |||
], | |||
"exclude": [ | |||
"**/node_modules/**" | |||
] | |||
} | |||
} | |||
} | |||
}, | |||
"angular-ngrx-course-e2e": { | |||
"root": "", | |||
"sourceRoot": "", | |||
"projectType": "application", | |||
"architect": { | |||
"e2e": { | |||
"builder": "@angular-devkit/build-angular:protractor", | |||
"options": { | |||
"protractorConfig": "./protractor.conf.js", | |||
"devServerTarget": "angular-ngrx-course:serve" | |||
} | |||
}, | |||
"lint": { | |||
"builder": "@angular-devkit/build-angular:tslint", | |||
"options": { | |||
"tsConfig": [ | |||
"e2e/tsconfig.e2e.json" | |||
], | |||
"exclude": [ | |||
"**/node_modules/**" | |||
] | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"defaultProject": "angular-ngrx-course", | |||
"schematics": { | |||
"@ngrx/schematics:component": { | |||
"prefix": "", | |||
"styleext": "scss" | |||
}, | |||
"@ngrx/schematics:directive": { | |||
"prefix": "" | |||
} | |||
}, | |||
"cli": { | |||
"defaultCollection": "@ngrx/schematics", | |||
"analytics": "78da76fb-38f9-4848-a7fe-1767a6672d4a" | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
import { browser, by, element } from 'protractor'; | |||
export class AppPage { | |||
navigateTo() { | |||
return browser.get('/'); | |||
} | |||
getParagraphText() { | |||
return element(by.css('app-root h1')).getText(); | |||
} | |||
} |
@ -0,0 +1,14 @@ | |||
{ | |||
"extends": "../tsconfig.base.json", | |||
"compilerOptions": { | |||
"outDir": "../out-tsc/e2e", | |||
"baseUrl": "./", | |||
"module": "commonjs", | |||
"target": "es5", | |||
"types": [ | |||
"jasmine", | |||
"jasminewd2", | |||
"node" | |||
] | |||
} | |||
} |
@ -0,0 +1,33 @@ | |||
// Karma configuration file, see link for more information | |||
// https://karma-runner.github.io/1.0/config/configuration-file.html | |||
module.exports = function (config) { | |||
config.set({ | |||
basePath: '', | |||
frameworks: ['jasmine', '@angular-devkit/build-angular'], | |||
plugins: [ | |||
require('karma-jasmine'), | |||
require('karma-chrome-launcher'), | |||
require('karma-jasmine-html-reporter'), | |||
require('karma-coverage-istanbul-reporter'), | |||
require('@angular-devkit/build-angular/plugins/karma') | |||
], | |||
client:{ | |||
clearContext: false // leave Jasmine Spec Runner output visible in browser | |||
}, | |||
coverageIstanbulReporter: { | |||
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], | |||
fixWebpackSourcePaths: true | |||
}, | |||
angularCli: { | |||
environment: 'dev' | |||
}, | |||
reporters: ['progress', 'kjhtml'], | |||
port: 9876, | |||
colors: true, | |||
logLevel: config.LOG_INFO, | |||
autoWatch: true, | |||
browsers: ['Chrome'], | |||
singleRun: false | |||
}); | |||
}; |
@ -0,0 +1,74 @@ | |||
{ | |||
"name": "sgeeks-starter-v2", | |||
"version": "0.0.2", | |||
"license": "MIT", | |||
"scripts": { | |||
"ng": "ng", | |||
"start": "ng serve --proxy-config ./proxy.json", | |||
"server": "ts-node -P ./server/server.tsconfig.json ./server/server.ts", | |||
"build": "ng build", | |||
"test": "ng test", | |||
"lint": "ng lint", | |||
"e2e": "ng e2e" | |||
}, | |||
"private": true, | |||
"dependencies": { | |||
"@angular-devkit/schematics": "^10.2.0", | |||
"@angular/animations": "^10.0.2", | |||
"@angular/cdk": "^10.0.1", | |||
"@angular/common": "^10.0.2", | |||
"@angular/compiler": "^10.0.2", | |||
"@angular/core": "^10.0.2", | |||
"@angular/flex-layout": "^10.0.0-beta.32", | |||
"@angular/forms": "^10.0.2", | |||
"@angular/material": "^10.0.1", | |||
"@angular/material-moment-adapter": "^10.0.1", | |||
"@angular/platform-browser": "^10.0.2", | |||
"@angular/platform-browser-dynamic": "^10.0.2", | |||
"@angular/router": "^10.0.2", | |||
"@ngrx/data": "^8.0.1", | |||
"@ngrx/effects": "^8.0.1", | |||
"@ngrx/entity": "^8.0.1", | |||
"@ngrx/router-store": "^8.0.1", | |||
"@ngrx/store": "^8.0.1", | |||
"@ngrx/store-devtools": "^8.0.1", | |||
"@swimlane/ngx-datatable": "^18.0.0", | |||
"body-parser": "^1.18.2", | |||
"core-js": "^2.4.1", | |||
"express": "^4.16.2", | |||
"highlight.js": "^10.3.1", | |||
"moment": "^2.22.2", | |||
"ng2-file-upload": "^1.4.0", | |||
"ngx-custom-validators": "^9.1.0", | |||
"ngx-perfect-scrollbar": "^10.0.1", | |||
"perfect-scrollbar": "^1.5.0", | |||
"rxjs": "^6.6.3", | |||
"tinycolor2": "^1.4.2", | |||
"tslib": "^2.0.0", | |||
"zone.js": "~0.10.3" | |||
}, | |||
"devDependencies": { | |||
"@angular-devkit/build-angular": "~0.1000.0", | |||
"@angular/cli": "^10.0.0", | |||
"@angular/compiler-cli": "^10.0.2", | |||
"@angular/language-service": "^10.0.2", | |||
"@ngrx/schematics": "^8.0.1", | |||
"@types/express": "^4.0.39", | |||
"@types/jasmine": "~2.5.53", | |||
"@types/jasminewd2": "~2.0.2", | |||
"@types/node": "^12.11.1", | |||
"codelyzer": "^5.1.2", | |||
"jasmine-core": "~3.5.0", | |||
"jasmine-spec-reporter": "~5.0.0", | |||
"karma": "~5.0.0", | |||
"karma-chrome-launcher": "~3.1.0", | |||
"karma-cli": "~1.0.1", | |||
"karma-coverage-istanbul-reporter": "~3.0.2", | |||
"karma-jasmine": "~3.3.0", | |||
"karma-jasmine-html-reporter": "^1.5.0", | |||
"protractor": "~7.0.0", | |||
"ts-node": "~3.2.0", | |||
"tslint": "~6.1.0", | |||
"typescript": "~3.9.5" | |||
} | |||
} |
@ -0,0 +1,28 @@ | |||
// Protractor configuration file, see link for more information | |||
// https://github.com/angular/protractor/blob/master/lib/config.ts | |||
const { SpecReporter } = require('jasmine-spec-reporter'); | |||
exports.config = { | |||
allScriptsTimeout: 11000, | |||
specs: [ | |||
'./e2e/**/*.e2e-spec.ts' | |||
], | |||
capabilities: { | |||
'browserName': 'chrome' | |||
}, | |||
directConnect: true, | |||
baseUrl: 'http://localhost:4200/', | |||
framework: 'jasmine', | |||
jasmineNodeOpts: { | |||
showColors: true, | |||
defaultTimeoutInterval: 30000, | |||
print: function() {} | |||
}, | |||
onPrepare() { | |||
require('ts-node').register({ | |||
project: 'e2e/tsconfig.e2e.json' | |||
}); | |||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); | |||
} | |||
}; |
@ -0,0 +1,6 @@ | |||
{ | |||
"/api": { | |||
"target": "http://localhost:9000", | |||
"secure": false | |||
} | |||
} |
@ -0,0 +1,26 @@ | |||
import {Request, Response} from 'express'; | |||
import {authenticate} from "./db-data"; | |||
export function loginUser(req: Request, res: Response) { | |||
console.log("User login attempt ..."); | |||
const {email, password} = req.body; | |||
const user = authenticate(email, password); | |||
if (user) { | |||
res.status(200).json({id:user.id, email: user.email}); | |||
} | |||
else { | |||
res.sendStatus(403); | |||
} | |||
} | |||
@ -0,0 +1,29 @@ | |||
import {Request, Response} from 'express'; | |||
import {COURSES} from './db-data'; | |||
export var coursesKeyCounter = 100; | |||
export function createCourse(req: Request, res: Response) { | |||
console.log("Creating new course ..."); | |||
const changes = req.body; | |||
const newCourse = { | |||
id: coursesKeyCounter, | |||
seqNo: coursesKeyCounter, | |||
...changes | |||
}; | |||
COURSES[newCourse.id] = newCourse; | |||
coursesKeyCounter += 1; | |||
setTimeout(() => { | |||
res.status(200).json(newCourse); | |||
}, 2000); | |||
} | |||
@ -0,0 +1,630 @@ | |||
export const USERS = { | |||
1: { | |||
id: 1, | |||
email: 'test@angular-university.io', | |||
password: 'test' | |||
} | |||
}; | |||
export const COURSES: any = { | |||
4: { | |||
id: 4, | |||
description: 'NgRx (with NgRx Data) - The Complete Guide', | |||
longDescription: 'Learn the modern Ngrx Ecosystem, including NgRx Data, Store, Effects, Router Store, Ngrx Entity, and Dev Tools.', | |||
iconUrl: 'https://angular-university.s3-us-west-1.amazonaws.com/course-images/ngrx-v2.png', | |||
category: 'BEGINNER', | |||
lessonsCount: 10, | |||
seqNo: 0, | |||
url: 'ngrx-course' | |||
}, | |||
2: { | |||
id: 2, | |||
description: 'Angular Core Deep Dive', | |||
longDescription: 'A detailed walk-through of the most important part of Angular - the Core and Common modules', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-core-in-depth-small.png', | |||
lessonsCount: 10, | |||
category: 'BEGINNER', | |||
seqNo: 1, | |||
url: 'angular-core-course' | |||
}, | |||
3: { | |||
id: 3, | |||
description: 'RxJs In Practice Course', | |||
longDescription: 'Understand the RxJs Observable pattern, learn the RxJs Operators via practical examples', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/rxjs-in-practice-course.png', | |||
category: 'BEGINNER', | |||
lessonsCount: 10, | |||
seqNo: 2, | |||
url: 'rxjs-course' | |||
}, | |||
1: { | |||
id: 1, | |||
description: 'Serverless Angular with Firebase Course', | |||
longDescription: 'Serveless Angular with Firestore, Firebase Storage & Hosting, Firebase Cloud Functions & AngularFire', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/serverless-angular-small.png', | |||
lessonsCount: 10, | |||
category: 'BEGINNER', | |||
seqNo: 4, | |||
url: 'serverless-angular' | |||
}, | |||
/* | |||
5: { | |||
id: 5, | |||
description: 'Angular for Beginners', | |||
longDescription: "Establish a solid layer of fundamentals, learn what's under the hood of Angular", | |||
iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/angular2-for-beginners-small-v2.png', | |||
category: 'BEGINNER', | |||
lessonsCount: 10, | |||
seqNo: 5, | |||
url: 'angular-for-beginners' | |||
}, | |||
*/ | |||
12: { | |||
id: 12, | |||
description: 'Angular Testing Course', | |||
longDescription: 'In-depth guide to Unit Testing and E2E Testing of Angular Applications', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-testing-small.png', | |||
category: 'BEGINNER', | |||
seqNo: 6, | |||
url: 'angular-testing-course', | |||
lessonsCount: 10, | |||
}, | |||
6: { | |||
id: 6, | |||
description: 'Angular Security Course - Web Security Fundamentals', | |||
longDescription: 'Learn Web Security Fundamentals and apply them to defend an Angular / Node Application from multiple types of attacks.', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/security-cover-small-v2.png', | |||
category: 'ADVANCED', | |||
lessonsCount: 11, | |||
seqNo: 7, | |||
url: 'angular-security-course' | |||
}, | |||
7: { | |||
id: 7, | |||
description: 'Angular PWA - Progressive Web Apps Course', | |||
longDescription: 'Learn Angular Progressive Web Applications, build the future of the Web Today.', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/angular-pwa-course.png', | |||
category: 'ADVANCED', | |||
lessonsCount: 8, | |||
seqNo: 8, | |||
url: 'angular-pwa-course' | |||
}, | |||
8: { | |||
id: 8, | |||
description: 'Angular Advanced Library Laboratory: Build Your Own Library', | |||
longDescription: 'Learn Advanced Angular functionality typically used in Library Development. Advanced Components, Directives, Testing, Npm', | |||
iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/advanced_angular-small-v3.png', | |||
category: 'ADVANCED', | |||
seqNo: 9, | |||
url: 'angular-advanced-course' | |||
}, | |||
9: { | |||
id: 9, | |||
description: 'The Complete Typescript Course', | |||
longDescription: 'Complete Guide to Typescript From Scratch: Learn the language in-depth and use it to build a Node REST API.', | |||
iconUrl: 'https://angular-academy.s3.amazonaws.com/thumbnails/typescript-2-small.png', | |||
category: 'BEGINNER', | |||
seqNo: 10, | |||
url: 'typescript-course' | |||
}, | |||
10: { | |||
id: 10, | |||
description: 'Rxjs and Reactive Patterns Angular Architecture Course', | |||
longDescription: 'Learn the core RxJs Observable Pattern as well and many other Design Patterns for building Reactive Angular Applications.', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-academy/blog/images/rxjs-reactive-patterns-small.png', | |||
category: 'BEGINNER', | |||
seqNo: 11, | |||
url: 'rxjs-patterns-course' | |||
}, | |||
11: { | |||
id: 11, | |||
description: 'Angular Material Course', | |||
longDescription: 'Build Applications with the official Angular Widget Library', | |||
iconUrl: 'https://s3-us-west-1.amazonaws.com/angular-university/course-images/material_design.png', | |||
category: 'BEGINNER', | |||
seqNo: 12, | |||
url: 'angular-material-course' | |||
} | |||
}; | |||
export const LESSONS = { | |||
1: { | |||
id: 1, | |||
'description': 'Angular Tutorial For Beginners - Build Your First App - Hello World Step By Step', | |||
'duration': '4:17', | |||
'seqNo': 1, | |||
courseId: 5 | |||
}, | |||
2: { | |||
id: 2, | |||
'description': 'Building Your First Component - Component Composition', | |||
'duration': '2:07', | |||
'seqNo': 2, | |||
courseId: 5 | |||
}, | |||
3: { | |||
id: 3, | |||
'description': 'Component @Input - How To Pass Input Data To an Component', | |||
'duration': '2:33', | |||
'seqNo': 3, | |||
courseId: 5 | |||
}, | |||
4: { | |||
id: 4, | |||
'description': ' Component Events - Using @Output to create custom events', | |||
'duration': '4:44', | |||
'seqNo': 4, | |||
courseId: 5 | |||
}, | |||
5: { | |||
id: 5, | |||
'description': ' Component Templates - Inline Vs External', | |||
'duration': '2:55', | |||
'seqNo': 5, | |||
courseId: 5 | |||
}, | |||
6: { | |||
id: 6, | |||
'description': 'Styling Components - Learn About Component Style Isolation', | |||
'duration': '3:27', | |||
'seqNo': 6, | |||
courseId: 5 | |||
}, | |||
7: { | |||
id: 7, | |||
'description': ' Component Interaction - Extended Components Example', | |||
'duration': '9:22', | |||
'seqNo': 7, | |||
courseId: 5 | |||
}, | |||
8: { | |||
id: 8, | |||
'description': ' Components Tutorial For Beginners - Components Exercise !', | |||
'duration': '1:26', | |||
'seqNo': 8, | |||
courseId: 5 | |||
}, | |||
9: { | |||
id: 9, | |||
'description': ' Components Tutorial For Beginners - Components Exercise Solution Inside', | |||
'duration': '2:08', | |||
'seqNo': 9, | |||
courseId: 5 | |||
}, | |||
10: { | |||
id: 10, | |||
'description': ' Directives - Inputs, Output Event Emitters and How To Export Template References', | |||
'duration': '4:01', | |||
'seqNo': 10, | |||
courseId: 5 | |||
}, | |||
// Security Course | |||
11: { | |||
id: 11, | |||
'description': 'Course Helicopter View', | |||
'duration': '08:19', | |||
'seqNo': 1, | |||
courseId: 6 | |||
}, | |||
12: { | |||
id: 12, | |||
'description': 'Installing Git, Node, NPM and Choosing an IDE', | |||
'duration': '04:17', | |||
'seqNo': 2, | |||
courseId: 6 | |||
}, | |||
13: { | |||
id: 13, | |||
'description': 'Installing The Lessons Code - Learn Why Its Essential To Use NPM 5', | |||
'duration': '06:05', | |||
'seqNo': 3, | |||
courseId: 6 | |||
}, | |||
14: { | |||
id: 14, | |||
'description': 'How To Run Node In TypeScript With Hot Reloading', | |||
'duration': '03:57', | |||
'seqNo': 4, | |||
courseId: 6 | |||
}, | |||
15: { | |||
id: 15, | |||
'description': 'Guided Tour Of The Sample Application', | |||
'duration': '06:00', | |||
'seqNo': 5, | |||
courseId: 6 | |||
}, | |||
16: { | |||
id: 16, | |||
'description': 'Client Side Authentication Service - API Design', | |||
'duration': '04:53', | |||
'seqNo': 6, | |||
courseId: 6 | |||
}, | |||
17: { | |||
id: 17, | |||
'description': 'Client Authentication Service - Design and Implementation', | |||
'duration': '09:14', | |||
'seqNo': 7, | |||
courseId: 6 | |||
}, | |||
18: { | |||
id: 18, | |||
'description': 'The New Angular HTTP Client - Doing a POST Call To The Server', | |||
'duration': '06:08', | |||
'seqNo': 8, | |||
courseId: 6 | |||
}, | |||
19: { | |||
id: 19, | |||
'description': 'User Sign Up Server-Side Implementation in Express', | |||
'duration': '08:50', | |||
'seqNo': 9, | |||
courseId: 6 | |||
}, | |||
20: { | |||
id: 20, | |||
'description': 'Introduction To Cryptographic Hashes - A Running Demo', | |||
'duration': '05:46', | |||
'seqNo': 10, | |||
courseId: 6 | |||
}, | |||
21: { | |||
id: 21, | |||
'description': 'Some Interesting Properties Of Hashing Functions - Validating Passwords', | |||
'duration': '06:31', | |||
'seqNo': 11, | |||
courseId: 6 | |||
}, | |||
// PWA course | |||
22: { | |||
id: 22, | |||
'description': 'Course Kick-Off - Install Node, NPM, IDE And Service Workers Section Code', | |||
'duration': '07:19', | |||
'seqNo': 1, | |||
courseId: 7 | |||
}, | |||
23: { | |||
id: 23, | |||
'description': 'Service Workers In a Nutshell - Service Worker Registration', | |||
'duration': '6:59', | |||
'seqNo': 2, | |||
courseId: 7 | |||
}, | |||
24: { | |||
id: 24, | |||
'description': 'Service Workers Hello World - Lifecycle Part 1 and PWA Chrome Dev Tools', | |||
'duration': '7:28', | |||
'seqNo': 3, | |||
courseId: 7 | |||
}, | |||
25: { | |||
id: 25, | |||
'description': 'Service Workers and Application Versioning - Install & Activate Lifecycle Phases', | |||
'duration': '10:17', | |||
'seqNo': 4, | |||
courseId: 7 | |||
}, | |||
26: { | |||
id: 26, | |||
'description': 'Downloading The Offline Page - The Service Worker Installation Phase', | |||
'duration': '09:50', | |||
'seqNo': 5, | |||
courseId: 7 | |||
}, | |||
27: { | |||
id: 27, | |||
'description': 'Introduction to the Cache Storage PWA API', | |||
'duration': '04:44', | |||
'seqNo': 6, | |||
courseId: 7 | |||
}, | |||
28: { | |||
id: 28, | |||
'description': 'View Service Workers HTTP Interception Features In Action', | |||
'duration': '06:07', | |||
'seqNo': 7, | |||
courseId: 7 | |||
}, | |||
29: { | |||
id: 29, | |||
'description': 'Service Workers Error Handling - Serving The Offline Page', | |||
'duration': '5:38', | |||
'seqNo': 8, | |||
courseId: 7 | |||
}, | |||
// Serverless Angular with Firebase Course | |||
30: { | |||
id: 30, | |||
description: 'Development Environment Setup', | |||
'duration': '5:38', | |||
'seqNo': 1, | |||
courseId: 1 | |||
}, | |||
31: { | |||
id: 31, | |||
description: 'Introduction to the Firebase Ecosystem', | |||
'duration': '5:12', | |||
'seqNo': 2, | |||
courseId: 1 | |||
}, | |||
32: { | |||
id: 32, | |||
description: 'Importing Data into Firestore', | |||
'duration': '4:07', | |||
'seqNo': 3, | |||
courseId: 1 | |||
}, | |||
33: { | |||
id: 33, | |||
description: 'Firestore Documents in Detail', | |||
'duration': '7:32', | |||
'seqNo': 4, | |||
courseId: 1 | |||
}, | |||
34: { | |||
id: 34, | |||
description: 'Firestore Collections in Detail', | |||
'duration': '6:28', | |||
'seqNo': 5, | |||
courseId: 1 | |||
}, | |||
35: { | |||
id: 35, | |||
description: 'Firestore Unique Identifiers', | |||
'duration': '4:38', | |||
'seqNo': 6, | |||
courseId: 1 | |||
}, | |||
36: { | |||
id: 36, | |||
description: 'Querying Firestore Collections', | |||
'duration': '7:54', | |||
'seqNo': 7, | |||
courseId: 1 | |||
}, | |||
37: { | |||
id: 37, | |||
description: 'Firebase Security Rules In Detail', | |||
'duration': '5:31', | |||
'seqNo': 8, | |||
courseId: 1 | |||
}, | |||
38: { | |||
id: 38, | |||
description: 'Firebase Cloud Functions In Detail', | |||
'duration': '8:19', | |||
'seqNo': 9, | |||
courseId: 1 | |||
}, | |||
39: { | |||
id: 39, | |||
description: 'Firebase Storage In Detail', | |||
'duration': '7:05', | |||
'seqNo': 10, | |||
courseId: 1 | |||
}, | |||
// Angular Testing Course | |||
40: { | |||
id: 40, | |||
description: 'Angular Testing Course - Helicopter View', | |||
'duration': '5:38', | |||
'seqNo': 1, | |||
courseId: 12 | |||
}, | |||
41: { | |||
id: 41, | |||
description: 'Setting Up the Development Environment', | |||
'duration': '5:12', | |||
'seqNo': 2, | |||
courseId: 12 | |||
}, | |||
42: { | |||
id: 42, | |||
description: 'Introduction to Jasmine, Spies and specs', | |||
'duration': '4:07', | |||
'seqNo': 3, | |||
courseId: 12 | |||
}, | |||
43: { | |||
id: 43, | |||
description: 'Introduction to Service Testing', | |||
'duration': '7:32', | |||
'seqNo': 4, | |||
courseId: 12 | |||
}, | |||
44: { | |||
id: 44, | |||
description: 'Settting up the Angular TestBed', | |||
'duration': '6:28', | |||
'seqNo': 5, | |||
courseId: 12 | |||
}, | |||
45: { | |||
id: 45, | |||
description: 'Mocking Angular HTTP requests', | |||
'duration': '4:38', | |||
'seqNo': 6, | |||
courseId: 12 | |||
}, | |||
46: { | |||
id: 46, | |||
description: 'Simulating Failing HTTP Requests', | |||
'duration': '7:54', | |||
'seqNo': 7, | |||
courseId: 12 | |||
}, | |||
47: { | |||
id: 47, | |||
description: 'Introduction to Angular Component Testing', | |||
'duration': '5:31', | |||
'seqNo': 8, | |||
courseId: 12 | |||
}, | |||
48: { | |||
id: 48, | |||
description: 'Testing Angular Components without the DOM', | |||
'duration': '8:19', | |||
'seqNo': 9, | |||
courseId: 12 | |||
}, | |||
49: { | |||
id: 49, | |||
description: 'Testing Angular Components with the DOM', | |||
'duration': '7:05', | |||
'seqNo': 10, | |||
courseId: 12 | |||
}, | |||
// Ngrx Course | |||
50: { | |||
id: 50, | |||
"description": "Welcome to the Angular Ngrx Course", | |||
"duration": "6:53", | |||
"seqNo": 1, | |||
courseId: 4 | |||
}, | |||
51: { | |||
id: 51, | |||
"description": "The Angular Ngrx Architecture Course - Helicopter View", | |||
"duration": "5:52", | |||
"seqNo": 2, | |||
courseId: 4 | |||
}, | |||
52: { | |||
id: 52, | |||
"description": "The Origins of Flux - Understanding the Famous Facebook Bug Problem", | |||
"duration": "8:17", | |||
"seqNo": 3, | |||
courseId: 4 | |||
}, | |||
53: { | |||
id: 53, | |||
"description": "Custom Global Events - Why Don't They Scale In Complexity?", | |||
"duration": "7:47", | |||
"seqNo": 4, | |||
courseId: 4 | |||
}, | |||
54: { | |||
id: 54, | |||
"description": "The Flux Architecture - How Does it Solve Facebook Counter Problem?", | |||
"duration": "9:22", | |||
"seqNo": 5, | |||
courseId: 4 | |||
}, | |||
55: { | |||
id: 55, | |||
"description": "Unidirectional Data Flow And The Angular Development Mode", | |||
"duration": "7:07", | |||
"seqNo": 6, | |||
courseId: 4 | |||
}, | |||
56: { | |||
id: 56, | |||
"description": "Dispatching an Action - Implementing the Login Component", | |||
"duration": "4:39", | |||
"seqNo": 7, | |||
courseId: 4 | |||
}, | |||
57: { | |||
id: 57, | |||
"description": "Setting Up the Ngrx DevTools - Demo", | |||
"duration": "4:44", | |||
"seqNo": 8, | |||
courseId: 4 | |||
}, | |||
58: { | |||
id: 58, | |||
"description": "Understanding Reducers - Writing Our First Reducer", | |||
"duration": "9:10", | |||
"seqNo": 9, | |||
courseId: 4 | |||
}, | |||
59: { | |||
id: 59, | |||
"description": "How To Define the Store Initial AppState", | |||
"duration": "9:10", | |||
"seqNo": 10, | |||
courseId: 4 | |||
} | |||
}; | |||
export function findCourseById(courseId: number) { | |||
return COURSES[courseId]; | |||
} | |||
export function findLessonsForCourse(courseId: number) { | |||
return Object.values(LESSONS).filter(lesson => lesson.courseId == courseId); | |||
} | |||
export function authenticate(email: string, password: string) { | |||
const user: any = Object.values(USERS).find(user => user.email === email); | |||
if (user && user.password == password) { | |||
return user; | |||
} else { | |||
return undefined; | |||
} | |||
} |
@ -0,0 +1,22 @@ | |||
import {Request, Response} from 'express'; | |||
import {COURSES} from "./db-data"; | |||
export function deleteCourse(req: Request, res: Response) { | |||
console.log("Deleting course ..."); | |||
const id = req.params["id"]; | |||
const course = COURSES[id]; | |||
delete COURSES[id]; | |||
setTimeout(() => { | |||
res.status(200).json({id}); | |||
}, 2000); | |||
} | |||
@ -0,0 +1,38 @@ | |||
import {Request, Response} from 'express'; | |||
import {COURSES} from "./db-data"; | |||
export function getAllCourses(req: Request, res: Response) { | |||
console.log("Retrieving courses data ..."); | |||
setTimeout(() => { | |||
res.status(200).json({payload:Object.values(COURSES)}); | |||
}, 1000); | |||
} | |||
export function getCourseByUrl(req: Request, res: Response) { | |||
const courseUrl = req.params["courseUrl"]; | |||
const courses:any = Object.values(COURSES); | |||
const course = courses.find(course => course.url == courseUrl); | |||
setTimeout(() => { | |||
res.status(200).json(course); | |||
}, 1000); | |||
} |
@ -0,0 +1,24 @@ | |||
import {Request, Response} from 'express'; | |||
import {COURSES} from "./db-data"; | |||
export function saveCourse(req: Request, res: Response) { | |||
console.log("Saving course ..."); | |||
const id = req.params["id"], | |||
changes = req.body; | |||
COURSES[id] = { | |||
...COURSES[id], | |||
...changes | |||
}; | |||
setTimeout(() => { | |||
res.status(200).json(COURSES[id]); | |||
}, 2000); | |||
} | |||
@ -0,0 +1,40 @@ | |||
import {Request, Response} from 'express'; | |||
import {LESSONS} from "./db-data"; | |||
import {setTimeout} from "timers"; | |||
export function searchLessons(req: Request, res: Response) { | |||
console.log('Searching for lessons ...'); | |||
const queryParams = req.query; | |||
const courseId = queryParams.courseId, | |||
filter = queryParams.filter || '', | |||
sortOrder = queryParams.sortOrder || 'asc', | |||
pageNumber = parseInt(queryParams.pageNumber) || 0, | |||
pageSize = parseInt(queryParams.pageSize); | |||
let lessons = Object.values(LESSONS).filter(lesson => lesson.courseId == courseId).sort((l1, l2) => l1.id - l2.id); | |||
if (filter) { | |||
lessons = lessons.filter(lesson => lesson.description.trim().toLowerCase().search(filter.toLowerCase()) >= 0); | |||
} | |||
if (sortOrder == "desc") { | |||
lessons = lessons.reverse(); | |||
} | |||
const initialPos = pageNumber * pageSize; | |||
console.log(`Retrieving lessons page starting at position ${initialPos}, page size ${pageSize} for course ${courseId}`); | |||
const lessonsPage = lessons.slice(initialPos, initialPos + pageSize); | |||
res.status(200).json(lessonsPage); | |||
} |
@ -0,0 +1,45 @@ | |||
import * as express from 'express'; | |||
import {Application} from "express"; | |||
import {getAllCourses, getCourseByUrl} from "./get-courses.route"; | |||
import {searchLessons} from "./search-lessons.route"; | |||
import {loginUser} from "./auth.route"; | |||
import {saveCourse} from "./save-course.route"; | |||
import {createCourse} from './create-course.route'; | |||
import {deleteCourse} from './delete-course.route'; | |||
const bodyParser = require('body-parser'); | |||
const app: Application = express(); | |||
app.use(bodyParser.json()); | |||
app.route('/api/login').post(loginUser); | |||
app.route('/api/courses').get(getAllCourses); | |||
app.route('/api/course').post(createCourse); | |||
app.route('/api/course/:id').put(saveCourse); | |||
app.route('/api/course/:id').delete(deleteCourse); | |||
app.route('/api/courses/:courseUrl').get(getCourseByUrl); | |||
app.route('/api/lessons').get(searchLessons); | |||
const httpServer:any = app.listen(9000, () => { | |||
console.log("HTTP REST API Server running at http://localhost:" + httpServer.address().port); | |||
}); | |||
@ -0,0 +1,6 @@ | |||
{ | |||
"compilerOptions": { | |||
"module": "commonjs", | |||
"lib": ["es2017"] | |||
} | |||
} |
@ -0,0 +1,17 @@ | |||
>>> body { | |||
margin: 0; | |||
} | |||
main { | |||
margin: 30px; | |||
} | |||
.menu-button { | |||
background: rgba(255, 170, 0, 0.76); | |||
color: white; | |||
border: none; | |||
cursor:pointer; | |||
outline:none; | |||
} | |||
@ -0,0 +1 @@ | |||
<router-outlet></router-outlet> |
@ -0,0 +1,73 @@ | |||
import {Component, OnInit} from '@angular/core'; | |||
import {select, Store} from '@ngrx/store'; | |||
import {Observable} from 'rxjs'; | |||
import {distinctUntilChanged, map} from 'rxjs/operators'; | |||
import {NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Router} from '@angular/router'; | |||
import {AppState} from './reducers'; | |||
import {isLoggedIn, isLoggedOut} from './auth/auth.selectors'; | |||
import {login, logout} from './auth/auth.actions'; | |||
@Component({ | |||
selector: 'app-root', | |||
templateUrl: './app.component.html', | |||
styleUrls: ['./app.component.css'] | |||
}) | |||
export class AppComponent implements OnInit { | |||
loading = true; | |||
isLoggedIn$: Observable<boolean>; | |||
isLoggedOut$: Observable<boolean>; | |||
constructor(private router: Router, | |||
private store: Store<AppState>) { | |||
} | |||
ngOnInit() { | |||
// const userProfile = localStorage.getItem("user"); | |||
// if (userProfile) { | |||
// this.store.dispatch(login({user: JSON.parse(userProfile)})); | |||
// } | |||
// this.router.events.subscribe(event => { | |||
// switch (true) { | |||
// case event instanceof NavigationStart: { | |||
// this.loading = true; | |||
// break; | |||
// } | |||
// case event instanceof NavigationEnd: | |||
// case event instanceof NavigationCancel: | |||
// case event instanceof NavigationError: { | |||
// this.loading = false; | |||
// break; | |||
// } | |||
// default: { | |||
// break; | |||
// } | |||
// } | |||
// }); | |||
// this.isLoggedIn$ = this.store | |||
// .pipe( | |||
// select(isLoggedIn) | |||
// ); | |||
// this.isLoggedOut$ = this.store | |||
// .pipe( | |||
// select(isLoggedOut) | |||
// ); | |||
} | |||
logout() { | |||
this.store.dispatch(logout()); | |||
} | |||
} |
@ -0,0 +1,71 @@ | |||
import {BrowserModule} from '@angular/platform-browser'; | |||
import {NgModule} from '@angular/core'; | |||
import {AppComponent} from './app.component'; | |||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; | |||
import {MatMenuModule} from '@angular/material/menu'; | |||
import {MatIconModule} from '@angular/material/icon'; | |||
import {MatListModule} from '@angular/material/list'; | |||
import {MatSidenavModule} from '@angular/material/sidenav'; | |||
import {MatToolbarModule} from '@angular/material/toolbar'; | |||
import {HttpClientModule} from '@angular/common/http'; | |||
import {RouterModule, Routes} from '@angular/router'; | |||
import {AuthModule} from './auth/auth.module'; | |||
import {StoreModule} from '@ngrx/store'; | |||
import {StoreDevtoolsModule} from '@ngrx/store-devtools'; | |||
import {environment} from '../environments/environment'; | |||
import {RouterState, StoreRouterConnectingModule} from '@ngrx/router-store'; | |||
import {EffectsModule} from '@ngrx/effects'; | |||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | |||
import {metaReducers, reducers} from './reducers'; | |||
import {AuthGuard} from './auth/auth.guard'; | |||
import {EntityDataModule} from '@ngrx/data'; | |||
import { rootRouterConfig } from './app.routing'; | |||
import { SharedModule } from './shared/shared.module'; | |||
@NgModule({ | |||
declarations: [ | |||
AppComponent | |||
], | |||
imports: [ | |||
BrowserModule, | |||
BrowserAnimationsModule, | |||
HttpClientModule, | |||
MatMenuModule, | |||
MatIconModule, | |||
MatSidenavModule, | |||
MatProgressSpinnerModule, | |||
MatListModule, | |||
MatToolbarModule, | |||
SharedModule, | |||
//AuthModule.forRoot(), | |||
StoreModule.forRoot(reducers, { | |||
metaReducers, | |||
runtimeChecks : { | |||
strictStateImmutability: true, | |||
strictActionImmutability: true, | |||
strictActionSerializability: true, | |||
strictStateSerializability: true | |||
} | |||
}), | |||
StoreDevtoolsModule.instrument({maxAge: 25, logOnly: environment.production}), | |||
EffectsModule.forRoot([]), | |||
EntityDataModule.forRoot({}), | |||
StoreRouterConnectingModule.forRoot({ | |||
stateKey: 'router', | |||
routerState: RouterState.Minimal | |||
}), | |||
RouterModule.forRoot(rootRouterConfig, { useHash: false }) | |||
], | |||
bootstrap: [AppComponent], | |||
providers:[AuthGuard] | |||
}) | |||
export class AppModule { | |||
} |
@ -0,0 +1,80 @@ | |||
import { Routes } from '@angular/router'; | |||
import { AdminLayoutComponent } from './shared/components/layouts/admin-layout/admin-layout.component'; | |||
import { AuthLayoutComponent } from './shared/components/layouts/auth-layout/auth-layout.component'; | |||
import { AuthGuard } from './shared/guards/auth.guard'; | |||
export const rootRouterConfig: Routes = [ | |||
{ | |||
path: '', | |||
redirectTo: '/dashboard/analytics', | |||
pathMatch: 'full', | |||
}, | |||
{ | |||
path: '', | |||
component: AdminLayoutComponent, | |||
canActivate: [AuthGuard], | |||
children: [ | |||
{ | |||
path: 'dashboard', | |||
loadChildren: () => | |||
import('./views/dashboard/dashboard.module').then( | |||
(m) => m.DashboardModule | |||
), | |||
}, | |||
{ | |||
path: 'mat-kits', | |||
loadChildren: () => | |||
import('./views/material-components/material-components.module').then( | |||
(m) => m.MaterialComponentsModule | |||
), | |||
data: { title: 'Material Coponents', breadcrumb: 'Material Coponents' }, | |||
}, | |||
{ | |||
path: 'pages', | |||
loadChildren: () => | |||
import('./views/others/others.module').then((m) => m.OthersModule), | |||
data: { title: 'Pages', breadcrumb: 'Pages' }, | |||
}, | |||
{ | |||
path: 'tables', | |||
loadChildren: () => | |||
import('./views/tables/tables.module').then((m) => m.TablesModule), | |||
data: { title: 'Tables', breadcrumb: 'Tables' }, | |||
}, | |||
{ | |||
path: 'forms', | |||
loadChildren: () => | |||
import('./views/forms/forms.module').then((m) => m.AppFormsModule), | |||
data: { title: 'Forms', breadcrumb: 'Forms' }, | |||
}, | |||
{ | |||
path: 'search', | |||
loadChildren: () => | |||
import('./views/search-view/search-view.module').then( | |||
(m) => m.SearchViewModule | |||
), | |||
}, | |||
{ | |||
path: 'orders', | |||
loadChildren: () => | |||
import('./views/order/order.module').then((m) => m.OrderModule), | |||
data: { title: 'Orders', breadcrumb: 'Orders' }, | |||
}, | |||
{ | |||
path: 'icons', | |||
loadChildren: () => | |||
import('./views/mat-icons/mat-icons.module').then( | |||
(m) => m.MatIconsModule | |||
), | |||
data: { title: 'Icons', breadcrumb: 'Mat icons' }, | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '**', | |||
redirectTo: 'sessions/404', | |||
}, | |||
]; |
@ -0,0 +1,5 @@ | |||
import * as AuthActions from './auth.actions'; | |||
export {AuthActions}; |
@ -0,0 +1,14 @@ | |||
import {createAction, props} from '@ngrx/store'; | |||
import {User} from './model/user.model'; | |||
export const login = createAction( | |||
"[Login Page] User Login", | |||
props<{user: User}>() | |||
); | |||
export const logout = createAction( | |||
"[Top Menu] Logout" | |||
); |
@ -0,0 +1,39 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {Actions, createEffect, ofType} from '@ngrx/effects'; | |||
import {AuthActions} from './action-types'; | |||
import {tap} from 'rxjs/operators'; | |||
import {Router} from '@angular/router'; | |||
@Injectable() | |||
export class AuthEffects { | |||
login$ = createEffect(() => | |||
this.actions$ | |||
.pipe( | |||
ofType(AuthActions.login), | |||
tap(action => localStorage.setItem('user', | |||
JSON.stringify(action.user)) | |||
) | |||
) | |||
, | |||
{dispatch: false}); | |||
logout$ = createEffect(() => | |||
this.actions$ | |||
.pipe( | |||
ofType(AuthActions.logout), | |||
tap(action => { | |||
localStorage.removeItem('user'); | |||
this.router.navigateByUrl('/login'); | |||
}) | |||
) | |||
, {dispatch: false}); | |||
constructor(private actions$: Actions, | |||
private router: Router) { | |||
} | |||
} |
@ -0,0 +1,37 @@ | |||
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; | |||
import {Injectable} from '@angular/core'; | |||
import {Observable} from 'rxjs'; | |||
import {AppState} from '../reducers'; | |||
import {select, Store} from '@ngrx/store'; | |||
import {isLoggedIn} from './auth.selectors'; | |||
import {tap} from 'rxjs/operators'; | |||
import {login, logout} from './auth.actions'; | |||
@Injectable() | |||
export class AuthGuard implements CanActivate { | |||
constructor( | |||
private store: Store<AppState>, | |||
private router: Router) { | |||
} | |||
canActivate( | |||
route: ActivatedRouteSnapshot, | |||
state: RouterStateSnapshot): Observable<boolean> { | |||
return this.store | |||
.pipe( | |||
select(isLoggedIn), | |||
tap(loggedIn => { | |||
if (!loggedIn) { | |||
this.router.navigateByUrl('/login'); | |||
} | |||
}) | |||
) | |||
} | |||
} |
@ -0,0 +1,51 @@ | |||
import {ModuleWithProviders, NgModule} from '@angular/core'; | |||
import {CommonModule} from '@angular/common'; | |||
import {LoginComponent} from './login/login.component'; | |||
import {MatCardModule} from '@angular/material/card'; | |||
import { MatInputModule } from '@angular/material/input'; | |||
import {RouterModule} from '@angular/router'; | |||
import {ReactiveFormsModule} from '@angular/forms'; | |||
import {MatButtonModule} from '@angular/material/button'; | |||
import { StoreModule } from '@ngrx/store'; | |||
import {AuthService} from './auth.service'; | |||
import * as fromAuth from './reducers'; | |||
import {authReducer} from './reducers'; | |||
import {AuthGuard} from './auth.guard'; | |||
import {EffectsModule} from '@ngrx/effects'; | |||
import {AuthEffects} from './auth.effects'; | |||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | |||
import { ButtonLoadingComponent } from 'app/shared/components/button-loading/button-loading.component'; | |||
import { SharedComponentsModule } from '../shared/components/shared-components.module'; | |||
import { SharedMaterialModule } from '../shared/shared-material.module'; | |||
import { FlexLayoutModule } from '@angular/flex-layout'; | |||
@NgModule({ | |||
imports: [ | |||
CommonModule, | |||
ReactiveFormsModule, | |||
MatCardModule, | |||
MatInputModule, | |||
MatButtonModule, | |||
SharedComponentsModule, | |||
BrowserAnimationsModule, | |||
SharedMaterialModule, | |||
FlexLayoutModule, | |||
RouterModule.forChild([{path: '', component: LoginComponent}]), | |||
StoreModule.forFeature('auth', authReducer), | |||
EffectsModule.forFeature([AuthEffects]), | |||
], | |||
declarations: [LoginComponent], | |||
exports: [LoginComponent] | |||
}) | |||
export class AuthModule { | |||
static forRoot(): ModuleWithProviders<AuthModule> { | |||
return { | |||
ngModule: AuthModule, | |||
providers: [ | |||
AuthService, | |||
AuthGuard | |||
] | |||
} | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
import {createFeatureSelector, createSelector} from '@ngrx/store'; | |||
import {AuthState} from './reducers'; | |||
export const selectAuthState = | |||
createFeatureSelector<AuthState>("auth"); | |||
export const isLoggedIn = createSelector( | |||
selectAuthState, | |||
auth => !!auth.user | |||
); | |||
export const isLoggedOut = createSelector( | |||
isLoggedIn, | |||
loggedIn => !loggedIn | |||
); |
@ -0,0 +1,20 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {HttpClient} from '@angular/common/http'; | |||
import {Observable} from 'rxjs'; | |||
import {User} from './model/user.model'; | |||
@Injectable() | |||
export class AuthService { | |||
constructor(private http: HttpClient) { | |||
} | |||
login(email:string, password: string): Observable<User> { | |||
return this.http.post<User>('/api/login', {email, password}); | |||
} | |||
} |
@ -0,0 +1,92 @@ | |||
<div | |||
class="height-100vh signup4-wrap" | |||
fxLayout="row wrap" | |||
fxLayoutAlign="center center" | |||
> | |||
<div | |||
class="signup4-container mat-elevation-z4 white" | |||
fxLayout="row wrap" | |||
fxLayout.xs="column" | |||
fxLayoutAlign="start stretch" | |||
fxFlex="60" | |||
fxFlex.xs="94" | |||
fxFlex.sm="80" | |||
[@animate]="{ | |||
value: '*', | |||
params: { y: '40px', opacity: '0', delay: '100ms', duration: '400ms' } | |||
}" | |||
> | |||
<!-- Left Side content --> | |||
<div | |||
fxLayout="column" | |||
fxLayoutAlign="center center" | |||
class="signup4-header" | |||
fxFlex="40" | |||
> | |||
<div class="" fxLayout="row wrap" fxLayoutAlign="center center"> | |||
<img | |||
width="200px" | |||
src="assets/images/illustrations/lighthouse.svg" | |||
alt="" | |||
/> | |||
</div> | |||
</div> | |||
<!-- Right side content --> | |||
<div fxFlex="60" fxLayout="row wrap" fxLayoutAlign="center center"> | |||
<form | |||
[formGroup]="signinForm" | |||
class="signup4-form grey-100" | |||
(ngSubmit)="signin()" | |||
> | |||
<mat-form-field class="full-width" appearance="outline"> | |||
<mat-label>Email</mat-label> | |||
<input | |||
matInput | |||
formControlName="username" | |||
type="text" | |||
name="username" | |||
placeholder="Username" | |||
/> | |||
</mat-form-field> | |||
<mat-form-field class="full-width" appearance="outline"> | |||
<mat-label>Password</mat-label> | |||
<input | |||
matInput | |||
formControlName="password" | |||
type="password" | |||
name="password" | |||
placeholder="********" | |||
/> | |||
</mat-form-field> | |||
<div | |||
fxLayout="row wrap" | |||
fxLayoutAlign="start center" | |||
style="margin-top: 20px;" | |||
> | |||
<button-loading [loading]="loading" loadingText="Signing in..." class="mr-16" color="primary" | |||
>Sign in</button-loading | |||
> | |||
<span class="px-16">or</span> | |||
<a | |||
class="font-weight-bold mat-color-primary" | |||
routerLink="/sessions/signup" | |||
>Sign Up</a | |||
> | |||
</div> | |||
<!-- <div fxLayout="row wrap" fxLayoutAlign="space-between center" style="margin-top: 20px"> | |||
<span>or connect with </span> | |||
<div> | |||
icons goes here | |||
</div> | |||
</div> --> | |||
</form> | |||
</div> | |||
</div> | |||
</div> |
@ -0,0 +1,11 @@ | |||
.login-page { | |||
max-width: 350px; | |||
margin: 50px auto 0 auto; | |||
} | |||
.login-form { | |||
display: flex; | |||
flex-direction: column; | |||
} |
@ -0,0 +1,152 @@ | |||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; | |||
import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; | |||
import {Store} from '@ngrx/store'; | |||
import {AuthService} from '../auth.service'; | |||
import {takeUntil, tap} from 'rxjs/operators'; | |||
import {noop, Subject} from 'rxjs'; | |||
import {ActivatedRoute, Router} from '@angular/router'; | |||
import {AppState} from '../../reducers'; | |||
import {login} from '../auth.actions'; | |||
import {AuthActions} from '../action-types'; | |||
import { matxAnimations } from 'app/shared/animations/matx-animations'; | |||
import { AppLoaderService } from 'app/shared/services/app-loader/app-loader.service'; | |||
@Component({ | |||
selector: 'app-login', | |||
templateUrl: './login.component.html', | |||
styleUrls: ['./login.component.scss'], | |||
animations: matxAnimations | |||
}) | |||
export class LoginComponent implements OnInit { | |||
signinForm: FormGroup; | |||
errorMsg = ''; | |||
return: string; | |||
loading: Boolean; | |||
private _unsubscribeAll: Subject<any>; | |||
constructor( | |||
private auth: AuthService, | |||
private matxLoader: AppLoaderService, | |||
private router: Router, | |||
private route: ActivatedRoute, | |||
private store: Store<AppState> | |||
) { | |||
this._unsubscribeAll = new Subject(); | |||
} | |||
ngOnInit() { | |||
this.signinForm = new FormGroup({ | |||
username: new FormControl('Watson', Validators.required), | |||
password: new FormControl('12345678', Validators.required), | |||
//rememberMe: new FormControl(true) | |||
}); | |||
this.route.queryParams | |||
.pipe(takeUntil(this._unsubscribeAll)) | |||
.subscribe(params => this.return = params['return'] || '/'); | |||
} | |||
ngAfterViewInit() { | |||
// setTimeout(() => { | |||
//this.autoSignIn(); | |||
// }) | |||
} | |||
// tslint:disable-next-line: use-lifecycle-interface | |||
ngOnDestroy() { | |||
this._unsubscribeAll.next(); | |||
this._unsubscribeAll.complete(); | |||
} | |||
signin() { | |||
// const signinData = this.signinForm.value; | |||
// this.loading = true; | |||
// this.jwtAuth.signin(signinData.username, signinData.password) | |||
// .subscribe(response => { | |||
// this.loading = false; | |||
// this.router.navigateByUrl(this.return); | |||
// }, err => { | |||
// this.loading = false; | |||
// this.errorMsg = err.message; | |||
// }) | |||
const val = this.signinForm.value; | |||
this.auth.login(val.email, val.password) | |||
.pipe( | |||
tap(user => { | |||
console.log(user); | |||
this.store.dispatch(login({user})); | |||
this.router.navigateByUrl('/dashboard/analytics'); | |||
}) | |||
) | |||
.subscribe( | |||
noop, | |||
() => alert('Login Failed') | |||
); | |||
} | |||
autoSignIn() { | |||
if (this.return === '/') { | |||
return; | |||
} | |||
this.matxLoader.open(`Automatically Signing you in! \n Return url: ${this.return.substring(0, 20)}...`, {width: '320px'}); | |||
setTimeout(() => { | |||
this.signin(); | |||
console.log('autoSignIn'); | |||
this.matxLoader.close(); | |||
}, 2000); | |||
} | |||
// form: FormGroup; | |||
// constructor( | |||
// private fb: FormBuilder, | |||
// private auth: AuthService, | |||
// private router: Router, | |||
// private store: Store<AppState>) { | |||
// this.form = fb.group({ | |||
// email: ['test@angular-university.io', [Validators.required]], | |||
// password: ['test', [Validators.required]] | |||
// }); | |||
// } | |||
// ngOnInit() { | |||
// } | |||
// login() { | |||
// const val = this.form.value; | |||
// this.auth.login(val.email, val.password) | |||
// .pipe( | |||
// tap(user => { | |||
// console.log(user); | |||
// this.store.dispatch(login({user})); | |||
// this.router.navigateByUrl('/dashboard/analytics'); | |||
// }) | |||
// ) | |||
// .subscribe( | |||
// noop, | |||
// () => alert('Login Failed') | |||
// ); | |||
// } | |||
} | |||
@ -0,0 +1,6 @@ | |||
export interface User { | |||
id: string; | |||
email: string; | |||
} |
@ -0,0 +1,40 @@ | |||
import { | |||
ActionReducer, | |||
ActionReducerMap, | |||
createFeatureSelector, createReducer, | |||
createSelector, | |||
MetaReducer, on | |||
} from '@ngrx/store'; | |||
import {User} from '../model/user.model'; | |||
import {AuthActions} from '../action-types'; | |||
export interface AuthState { | |||
user: User | |||
} | |||
export const initialAuthState: AuthState = { | |||
user: undefined | |||
}; | |||
export const authReducer = createReducer( | |||
initialAuthState, | |||
on(AuthActions.login, (state, action) => { | |||
return { | |||
user: action.user | |||
} | |||
}), | |||
on(AuthActions.logout, (state, action) => { | |||
return { | |||
user: undefined | |||
} | |||
}) | |||
); | |||
@ -0,0 +1,56 @@ | |||
.course { | |||
text-align: center; | |||
max-width: 390px; | |||
margin: 0 auto; | |||
} | |||
.course-thumbnail { | |||
width: 175px; | |||
margin: 20px auto 0 auto; | |||
display: block; | |||
border-radius: 4px; | |||
} | |||
.description-cell { | |||
text-align: left; | |||
margin: 10px auto; | |||
} | |||
.duration-cell { | |||
text-align: center; | |||
} | |||
.duration-cell mat-icon { | |||
display: inline-block; | |||
vertical-align: middle; | |||
font-size: 20px; | |||
} | |||
.lessons-table { | |||
min-height: 360px; | |||
margin-top: 10px; | |||
} | |||
.spinner-container mat-spinner { | |||
margin: 95px auto 0 auto; | |||
} | |||
.action-toolbar { | |||
margin-top: 20px; | |||
} | |||
h2 { | |||
font-family: "Roboto"; | |||
} | |||
.bottom-toolbar { | |||
margin-top: 20px; | |||
margin-bottom: 200px; | |||
} | |||
.spinner-container { | |||
width:390px; | |||
} |
@ -0,0 +1,51 @@ | |||
<div class="course" *ngIf="(course$ | async) as course"> | |||
<h2>{{course?.description}}</h2> | |||
<img class="course-thumbnail mat-elevation-z8" [src]="course?.iconUrl"> | |||
<div class="spinner-container" *ngIf="(loading$ | async )"> | |||
<mat-spinner></mat-spinner> | |||
</div> | |||
<mat-table class="lessons-table mat-elevation-z8" [dataSource]="lessons$ | async"> | |||
<ng-container matColumnDef="seqNo"> | |||
<mat-header-cell *matHeaderCellDef>#</mat-header-cell> | |||
<mat-cell *matCellDef="let lesson">{{lesson.seqNo}}</mat-cell> | |||
</ng-container> | |||
<ng-container matColumnDef="description"> | |||
<mat-header-cell *matHeaderCellDef>Description</mat-header-cell> | |||
<mat-cell class="description-cell" | |||
*matCellDef="let lesson">{{lesson.description}}</mat-cell> | |||
</ng-container> | |||
<ng-container matColumnDef="duration"> | |||
<mat-header-cell *matHeaderCellDef>Duration</mat-header-cell> | |||
<mat-cell class="duration-cell" | |||
*matCellDef="let lesson">{{lesson.duration}}</mat-cell> | |||
</ng-container> | |||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> | |||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> | |||
</mat-table> | |||
<button class="bottom-toolbar" mat-raised-button color="accent" | |||
(click)="loadLessonsPage(course)">Load More</button> | |||
</div> |
@ -0,0 +1,73 @@ | |||
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; | |||
import {ActivatedRoute} from '@angular/router'; | |||
import {Course} from '../model/course'; | |||
import {Observable, of} from 'rxjs'; | |||
import {Lesson} from '../model/lesson'; | |||
import {concatMap, delay, filter, first, map, shareReplay, tap, withLatestFrom} from 'rxjs/operators'; | |||
import {CoursesHttpService} from '../services/courses-http.service'; | |||
import {CourseEntityService} from '../services/course-entity.service'; | |||
import {LessonEntityService} from '../services/lesson-entity.service'; | |||
@Component({ | |||
selector: 'course', | |||
templateUrl: './course.component.html', | |||
styleUrls: ['./course.component.css'], | |||
changeDetection: ChangeDetectionStrategy.OnPush | |||
}) | |||
export class CourseComponent implements OnInit { | |||
course$: Observable<Course>; | |||
loading$: Observable<boolean>; | |||
lessons$: Observable<Lesson[]>; | |||
displayedColumns = ['seqNo', 'description', 'duration']; | |||
nextPage = 0; | |||
constructor( | |||
private coursesService: CourseEntityService, | |||
private lessonsService: LessonEntityService, | |||
private route: ActivatedRoute) { | |||
} | |||
ngOnInit() { | |||
const courseUrl = this.route.snapshot.paramMap.get('courseUrl'); | |||
this.course$ = this.coursesService.entities$ | |||
.pipe( | |||
map(courses => courses.find(course => course.url == courseUrl)) | |||
); | |||
this.lessons$ = this.lessonsService.entities$ | |||
.pipe( | |||
withLatestFrom(this.course$), | |||
tap(([lessons, course]) => { | |||
if (this.nextPage == 0) { | |||
this.loadLessonsPage(course); | |||
} | |||
}), | |||
map(([lessons, course]) => | |||
lessons.filter(lesson => lesson.courseId == course.id)) | |||
); | |||
this.loading$ = this.lessonsService.loading$.pipe(delay(0)); | |||
} | |||
loadLessonsPage(course: Course) { | |||
this.lessonsService.getWithQuery({ | |||
'courseId': course.id.toString(), | |||
'pageNumber': this.nextPage.toString(), | |||
'pageSize': '3' | |||
}); | |||
this.nextPage += 1; | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
.course-card { | |||
margin: 20px 10px; | |||
} | |||
.course-actions { | |||
text-align: center; | |||
} | |||
.course-actions button { | |||
margin-right: 10px; | |||
} | |||
@ -0,0 +1,33 @@ | |||
<mat-card *ngFor="let course of courses" class="course-card mat-elevation-z10"> | |||
<mat-card-header> | |||
<mat-card-title>{{course.description}}</mat-card-title> | |||
</mat-card-header> | |||
<img mat-card-image [src]="course.iconUrl"> | |||
<mat-card-content> | |||
<p>{{course.longDescription}}</p> | |||
</mat-card-content> | |||
<mat-card-actions class="course-actions"> | |||
<button mat-raised-button color="primary" [routerLink]="['/courses', course.url]"> | |||
VIEW | |||
</button> | |||
<button mat-mini-fab color="accent"> | |||
<mat-icon class="add-course-btn" (click)="editCourse(course)">edit</mat-icon> | |||
</button> | |||
<button mat-mini-fab color="warn"> | |||
<mat-icon class="add-course-btn" (click)="onDeleteCourse(course)">delete</mat-icon> | |||
</button> | |||
</mat-card-actions> | |||
</mat-card> |
@ -0,0 +1,67 @@ | |||
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; | |||
import {Course} from "../model/course"; | |||
import { MatDialog, MatDialogConfig } from "@angular/material/dialog"; | |||
import {EditCourseDialogComponent} from "../edit-course-dialog/edit-course-dialog.component"; | |||
import {defaultDialogConfig} from '../shared/default-dialog-config'; | |||
import {CourseEntityService} from '../services/course-entity.service'; | |||
@Component({ | |||
selector: 'courses-card-list', | |||
templateUrl: './courses-card-list.component.html', | |||
styleUrls: ['./courses-card-list.component.css'], | |||
changeDetection: ChangeDetectionStrategy.OnPush | |||
}) | |||
export class CoursesCardListComponent implements OnInit { | |||
@Input() | |||
courses: Course[]; | |||
@Output() | |||
courseChanged = new EventEmitter(); | |||
constructor( | |||
private dialog: MatDialog, | |||
private courseService: CourseEntityService) { | |||
} | |||
ngOnInit() { | |||
} | |||
editCourse(course:Course) { | |||
const dialogConfig = defaultDialogConfig(); | |||
dialogConfig.data = { | |||
dialogTitle:"Edit Course", | |||
course, | |||
mode: 'update' | |||
}; | |||
this.dialog.open(EditCourseDialogComponent, dialogConfig) | |||
.afterClosed() | |||
.subscribe(() => this.courseChanged.emit()); | |||
} | |||
onDeleteCourse(course:Course) { | |||
this.courseService.delete(course) | |||
.subscribe( | |||
() => console.log("Delete completed"), | |||
err => console.log("Deleted failed", err) | |||
); | |||
} | |||
} | |||
@ -0,0 +1,120 @@ | |||
import {NgModule} from '@angular/core'; | |||
import {CommonModule} from '@angular/common'; | |||
import {HomeComponent} from './home/home.component'; | |||
import {CoursesCardListComponent} from './courses-card-list/courses-card-list.component'; | |||
import {EditCourseDialogComponent} from './edit-course-dialog/edit-course-dialog.component'; | |||
import {CoursesHttpService} from './services/courses-http.service'; | |||
import {CourseComponent} from './course/course.component'; | |||
import {MatDatepickerModule} from '@angular/material/datepicker'; | |||
import {MatDialogModule} from '@angular/material/dialog'; | |||
import {MatInputModule} from '@angular/material/input'; | |||
import {MatPaginatorModule} from '@angular/material/paginator'; | |||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; | |||
import {MatSelectModule} from '@angular/material/select'; | |||
import {MatSlideToggleModule} from '@angular/material/slide-toggle'; | |||
import {MatSortModule} from '@angular/material/sort'; | |||
import {MatTableModule} from '@angular/material/table'; | |||
import {MatTabsModule} from '@angular/material/tabs'; | |||
import {ReactiveFormsModule} from '@angular/forms'; | |||
import {MatMomentDateModule} from '@angular/material-moment-adapter'; | |||
import {MatCardModule} from '@angular/material/card'; | |||
import {MatButtonModule} from '@angular/material/button'; | |||
import {MatIconModule} from '@angular/material/icon'; | |||
import {RouterModule, Routes} from '@angular/router'; | |||
import {EntityDataService, EntityDefinitionService, EntityMetadataMap} from '@ngrx/data'; | |||
import {compareCourses, Course} from './model/course'; | |||
import {compareLessons, Lesson} from './model/lesson'; | |||
import {CourseEntityService} from './services/course-entity.service'; | |||
import {CoursesResolver} from './services/courses.resolver'; | |||
import {CoursesDataService} from './services/courses-data.service'; | |||
import {LessonEntityService} from './services/lesson-entity.service'; | |||
export const coursesRoutes: Routes = [ | |||
{ | |||
path: '', | |||
component: HomeComponent, | |||
resolve: { | |||
courses: CoursesResolver | |||
} | |||
}, | |||
{ | |||
path: ':courseUrl', | |||
component: CourseComponent, | |||
resolve: { | |||
courses: CoursesResolver | |||
} | |||
} | |||
]; | |||
const entityMetadata: EntityMetadataMap = { | |||
Course: { | |||
sortComparer: compareCourses, | |||
entityDispatcherOptions: { | |||
optimisticUpdate: true | |||
} | |||
}, | |||
Lesson: { | |||
sortComparer: compareLessons | |||
} | |||
}; | |||
@NgModule({ | |||
imports: [ | |||
CommonModule, | |||
MatButtonModule, | |||
MatIconModule, | |||
MatCardModule, | |||
MatTabsModule, | |||
MatInputModule, | |||
MatTableModule, | |||
MatPaginatorModule, | |||
MatSortModule, | |||
MatProgressSpinnerModule, | |||
MatSlideToggleModule, | |||
MatDialogModule, | |||
MatSelectModule, | |||
MatDatepickerModule, | |||
MatMomentDateModule, | |||
ReactiveFormsModule, | |||
RouterModule.forChild(coursesRoutes) | |||
], | |||
declarations: [ | |||
HomeComponent, | |||
CoursesCardListComponent, | |||
EditCourseDialogComponent, | |||
CourseComponent | |||
], | |||
exports: [ | |||
HomeComponent, | |||
CoursesCardListComponent, | |||
EditCourseDialogComponent, | |||
CourseComponent | |||
], | |||
entryComponents: [EditCourseDialogComponent], | |||
providers: [ | |||
CoursesHttpService, | |||
CourseEntityService, | |||
LessonEntityService, | |||
CoursesResolver, | |||
CoursesDataService | |||
] | |||
}) | |||
export class CoursesModule { | |||
constructor( | |||
private eds: EntityDefinitionService, | |||
private entityDataService: EntityDataService, | |||
private coursesDataService: CoursesDataService) { | |||
eds.registerMetadataMap(entityMetadata); | |||
entityDataService.registerService('Course', coursesDataService); | |||
} | |||
} |
@ -0,0 +1,10 @@ | |||
.mat-form-field { | |||
display: block; | |||
} | |||
textarea { | |||
height: 100px; | |||
resize: vertical; | |||
} |
@ -0,0 +1,95 @@ | |||
<h2 mat-dialog-title>{{dialogTitle}}</h2> | |||
<mat-dialog-content> | |||
<ng-container *ngIf="form"> | |||
<div class="spinner-container" *ngIf="loading$ | async"> | |||
<mat-spinner></mat-spinner> | |||
</div> | |||
<ng-container [formGroup]="form"> | |||
<mat-form-field> | |||
<input matInput | |||
placeholder="Course Description" | |||
formControlName="description"> | |||
</mat-form-field> | |||
<ng-container *ngIf="mode == 'create'"> | |||
<mat-form-field> | |||
<input matInput | |||
placeholder="Course Url" | |||
formControlName="url"> | |||
</mat-form-field> | |||
<mat-form-field> | |||
<input matInput | |||
placeholder="Course Icon Url" | |||
formControlName="iconUrl"> | |||
</mat-form-field> | |||
</ng-container> | |||
<mat-form-field> | |||
<mat-select placeholder="Select category" | |||
formControlName="category"> | |||
<mat-option value="BEGINNER"> | |||
Beginner</mat-option> | |||
<mat-option value="INTERMEDIATE"> | |||
Intermediate</mat-option> | |||
<mat-option value="ADVANCED"> | |||
Advanced</mat-option> | |||
</mat-select> | |||
</mat-form-field> | |||
<mat-slide-toggle formControlName="promo">Promotion On</mat-slide-toggle> | |||
<mat-form-field> | |||
<textarea matInput placeholder="Description" | |||
formControlName="longDescription"> | |||
</textarea> | |||
</mat-form-field> | |||
</ng-container> | |||
</ng-container> | |||
</mat-dialog-content> | |||
<mat-dialog-actions> | |||
<button mat-raised-button (click)="onClose()"> | |||
Close | |||
</button> | |||
<button mat-raised-button color="primary" | |||
[disabled]="!form?.valid || (this.loading$ | async)" | |||
(click)="onSave()"> | |||
Save | |||
</button> | |||
</mat-dialog-actions> | |||
@ -0,0 +1,91 @@ | |||
import {ChangeDetectionStrategy, Component, Inject} from '@angular/core'; | |||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; | |||
import {Course} from '../model/course'; | |||
import {FormBuilder, FormGroup, Validators} from '@angular/forms'; | |||
import {Observable} from 'rxjs'; | |||
import {CoursesHttpService} from '../services/courses-http.service'; | |||
import {CourseEntityService} from '../services/course-entity.service'; | |||
@Component({ | |||
selector: 'course-dialog', | |||
templateUrl: './edit-course-dialog.component.html', | |||
styleUrls: ['./edit-course-dialog.component.css'], | |||
changeDetection: ChangeDetectionStrategy.OnPush | |||
}) | |||
export class EditCourseDialogComponent { | |||
form: FormGroup; | |||
dialogTitle: string; | |||
course: Course; | |||
mode: 'create' | 'update'; | |||
loading$: Observable<boolean>; | |||
constructor( | |||
private fb: FormBuilder, | |||
private dialogRef: MatDialogRef<EditCourseDialogComponent>, | |||
@Inject(MAT_DIALOG_DATA) data, | |||
private coursesService: CourseEntityService) { | |||
this.dialogTitle = data.dialogTitle; | |||
this.course = data.course; | |||
this.mode = data.mode; | |||
const formControls = { | |||
description: ['', Validators.required], | |||
category: ['', Validators.required], | |||
longDescription: ['', Validators.required], | |||
promo: ['', []] | |||
}; | |||
if (this.mode == 'update') { | |||
this.form = this.fb.group(formControls); | |||
this.form.patchValue({...data.course}); | |||
} else if (this.mode == 'create') { | |||
this.form = this.fb.group({ | |||
...formControls, | |||
url: ['', Validators.required], | |||
iconUrl: ['', Validators.required] | |||
}); | |||
} | |||
} | |||
onClose() { | |||
this.dialogRef.close(); | |||
} | |||
onSave() { | |||
const course: Course = { | |||
...this.course, | |||
...this.form.value | |||
}; | |||
if (this.mode == 'update') { | |||
this.coursesService.update(course); | |||
this.dialogRef.close(); | |||
} else if (this.mode == 'create') { | |||
this.coursesService.add(course) | |||
.subscribe( | |||
newCourse => { | |||
console.log('New Course', newCourse); | |||
this.dialogRef.close(); | |||
} | |||
); | |||
} | |||
} | |||
} |
@ -0,0 +1,38 @@ | |||
.title { | |||
text-align: center; | |||
margin-right: 15px; | |||
} | |||
.courses-panel { | |||
max-width: 350px; | |||
margin: 0 auto; | |||
} | |||
.counters { | |||
display: flex; | |||
} | |||
.filler { | |||
flex: 1 1 auto; | |||
} | |||
h2 { | |||
font-family: "Roboto"; | |||
} | |||
.header { | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} | |||
.spinner-container { | |||
margin-top: 100px; | |||
} |
@ -0,0 +1,41 @@ | |||
<div class="courses-panel"> | |||
<div class="header"> | |||
<h2 class="title">All Courses</h2> | |||
<button mat-mini-fab> | |||
<mat-icon class="add-course-btn" (click)="onAddCourse()">add</mat-icon> | |||
</button> | |||
</div> | |||
<div class="counters"> | |||
<p>In Promo: {{promoTotal$ | async}}</p> | |||
</div> | |||
<mat-tab-group> | |||
<mat-tab label="Beginners"> | |||
<courses-card-list (courseChanged)="reload()" | |||
[courses]="beginnerCourses$ | async"> | |||
</courses-card-list> | |||
</mat-tab> | |||
<mat-tab label="Advanced"> | |||
<courses-card-list (courseChanged)="reload()" | |||
[courses]="advancedCourses$ | async" | |||
></courses-card-list> | |||
</mat-tab> | |||
</mat-tab-group> | |||
</div> | |||
@ -0,0 +1,68 @@ | |||
import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; | |||
import {Course} from '../model/course'; | |||
import {Observable} from 'rxjs'; | |||
import {defaultDialogConfig} from '../shared/default-dialog-config'; | |||
import {EditCourseDialogComponent} from '../edit-course-dialog/edit-course-dialog.component'; | |||
import { MatDialog } from '@angular/material/dialog'; | |||
import {map} from 'rxjs/operators'; | |||
import {CourseEntityService} from '../services/course-entity.service'; | |||
@Component({ | |||
selector: 'home', | |||
templateUrl: './home.component.html', | |||
styleUrls: ['./home.component.css'], | |||
changeDetection: ChangeDetectionStrategy.OnPush | |||
}) | |||
export class HomeComponent implements OnInit { | |||
promoTotal$: Observable<number>; | |||
beginnerCourses$: Observable<Course[]>; | |||
advancedCourses$: Observable<Course[]>; | |||
constructor( | |||
private dialog: MatDialog, | |||
private coursesService: CourseEntityService) { | |||
} | |||
ngOnInit() { | |||
this.reload(); | |||
} | |||
reload() { | |||
this.beginnerCourses$ = this.coursesService.entities$ | |||
.pipe( | |||
map(courses => courses.filter(course => course.category == 'BEGINNER')) | |||
); | |||
this.advancedCourses$ = this.coursesService.entities$ | |||
.pipe( | |||
map(courses => courses.filter(course => course.category == 'ADVANCED')) | |||
); | |||
this.promoTotal$ = this.coursesService.entities$ | |||
.pipe( | |||
map(courses => courses.filter(course => course.promo).length) | |||
); | |||
} | |||
onAddCourse() { | |||
const dialogConfig = defaultDialogConfig(); | |||
dialogConfig.data = { | |||
dialogTitle:"Create Course", | |||
mode: 'create' | |||
}; | |||
this.dialog.open(EditCourseDialogComponent, dialogConfig); | |||
} | |||
} |
@ -0,0 +1,28 @@ | |||
export interface Course { | |||
id: number; | |||
seqNo:number; | |||
url:string; | |||
iconUrl: string; | |||
courseListIcon: string; | |||
description: string; | |||
longDescription?: string; | |||
category: string; | |||
lessonsCount: number; | |||
promo: boolean; | |||
} | |||
export function compareCourses(c1:Course, c2: Course) { | |||
const compare = c1.seqNo - c2.seqNo; | |||
if (compare > 0) { | |||
return 1; | |||
} | |||
else if ( compare < 0) { | |||
return -1; | |||
} | |||
else return 0; | |||
} |
@ -0,0 +1,26 @@ | |||
export interface Lesson { | |||
id: number; | |||
description: string; | |||
duration: string; | |||
seqNo: number; | |||
courseId: number; | |||
} | |||
export function compareLessons(l1:Lesson, l2: Lesson) { | |||
const compareCourses = l1.courseId - l2.courseId; | |||
if (compareCourses > 0) { | |||
return 1; | |||
} | |||
else if (compareCourses < 0){ | |||
return -1; | |||
} | |||
else { | |||
return l1.seqNo - l2.seqNo; | |||
} | |||
} |
@ -0,0 +1,19 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {EntityCollectionServiceBase, EntityCollectionServiceElementsFactory} from '@ngrx/data'; | |||
import {Course} from '../model/course'; | |||
@Injectable() | |||
export class CourseEntityService | |||
extends EntityCollectionServiceBase<Course> { | |||
constructor( | |||
serviceElementsFactory: | |||
EntityCollectionServiceElementsFactory) { | |||
super('Course', serviceElementsFactory); | |||
} | |||
} | |||
@ -0,0 +1,26 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {DefaultDataService, HttpUrlGenerator} from '@ngrx/data'; | |||
import {Course} from '../model/course'; | |||
import {HttpClient} from '@angular/common/http'; | |||
import {Observable} from 'rxjs'; | |||
import {map} from 'rxjs/operators'; | |||
@Injectable() | |||
export class CoursesDataService extends DefaultDataService<Course> { | |||
constructor(http:HttpClient, httpUrlGenerator: HttpUrlGenerator) { | |||
super('Course', http, httpUrlGenerator); | |||
} | |||
getAll(): Observable<Course[]> { | |||
return this.http.get('/api/courses') | |||
.pipe( | |||
map(res => res["payload"]) | |||
); | |||
} | |||
} |
@ -0,0 +1,48 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {HttpClient, HttpParams} from '@angular/common/http'; | |||
import {Observable} from 'rxjs'; | |||
import {Course} from '../model/course'; | |||
import {map} from 'rxjs/operators'; | |||
import {Lesson} from '../model/lesson'; | |||
@Injectable() | |||
export class CoursesHttpService { | |||
constructor(private http:HttpClient) { | |||
} | |||
findAllCourses(): Observable<Course[]> { | |||
return this.http.get('/api/courses') | |||
.pipe( | |||
map(res => res['payload']) | |||
); | |||
} | |||
findCourseByUrl(courseUrl: string): Observable<Course> { | |||
return this.http.get<Course>(`/api/courses/${courseUrl}`); | |||
} | |||
findLessons( | |||
courseId:number, | |||
pageNumber = 0, pageSize = 3): Observable<Lesson[]> { | |||
return this.http.get<Lesson[]>('/api/lessons', { | |||
params: new HttpParams() | |||
.set('courseId', courseId.toString()) | |||
.set('sortOrder', 'asc') | |||
.set('pageNumber', pageNumber.toString()) | |||
.set('pageSize', pageSize.toString()) | |||
}); | |||
} | |||
saveCourse(courseId: string | number, changes: Partial<Course>) { | |||
return this.http.put('/api/course/' + courseId, changes); | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from '@angular/router'; | |||
import {Observable} from 'rxjs'; | |||
import {CourseEntityService} from './course-entity.service'; | |||
import {filter, first, map, tap} from 'rxjs/operators'; | |||
@Injectable() | |||
export class CoursesResolver implements Resolve<boolean> { | |||
constructor(private coursesService: CourseEntityService) { | |||
} | |||
resolve(route: ActivatedRouteSnapshot, | |||
state: RouterStateSnapshot): Observable<boolean> { | |||
return this.coursesService.loaded$ | |||
.pipe( | |||
tap(loaded => { | |||
if (!loaded) { | |||
this.coursesService.getAll(); | |||
} | |||
}), | |||
filter(loaded => !!loaded), | |||
first() | |||
); | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
import {Injectable} from '@angular/core'; | |||
import {EntityCollectionServiceBase, EntityCollectionServiceElementsFactory} from '@ngrx/data'; | |||
import {Lesson} from '../model/lesson'; | |||
@Injectable() | |||
export class LessonEntityService extends EntityCollectionServiceBase<Lesson> { | |||
constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { | |||
super('Lesson', serviceElementsFactory); | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
import { MatDialogConfig } from '@angular/material/dialog'; | |||
export function defaultDialogConfig() { | |||
const dialogConfig = new MatDialogConfig(); | |||
dialogConfig.disableClose = true; | |||
dialogConfig.autoFocus = true; | |||
dialogConfig.width = '400px'; | |||
return dialogConfig; | |||
} |
@ -0,0 +1,20 @@ | |||
import { | |||
ActionReducer, | |||
ActionReducerMap, | |||
createFeatureSelector, | |||
createSelector, | |||
MetaReducer | |||
} from '@ngrx/store'; | |||
import { environment } from '../../environments/environment'; | |||
import {routerReducer} from '@ngrx/router-store'; | |||
export interface AppState { | |||
} | |||
export const metaReducers: MetaReducer<AppState>[] = | |||
!environment.production ? [logger] : []; | |||
@ -0,0 +1,53 @@ | |||
import { | |||
trigger, | |||
animate, | |||
style, | |||
transition, | |||
state, | |||
animation, | |||
useAnimation | |||
} from "@angular/animations"; | |||
const reusable = animation( | |||
[ | |||
style({ | |||
opacity: "{{opacity}}", | |||
transform: "scale({{scale}}) translate3d({{x}}, {{y}}, {{z}})" | |||
}), | |||
animate("{{duration}} {{delay}} cubic-bezier(0.0, 0.0, 0.2, 1)", style("*")) | |||
], | |||
{ | |||
params: { | |||
duration: "200ms", | |||
delay: "0ms", | |||
opacity: "0", | |||
scale: "1", | |||
x: "0", | |||
y: "0", | |||
z: "0" | |||
} | |||
} | |||
); | |||
export const matxAnimations = [ | |||
trigger("animate", [transition("void => *", [useAnimation(reusable)])]), | |||
trigger("fadeInOut", [ | |||
state( | |||
"0", | |||
style({ | |||
opacity: 0, | |||
display: "none" | |||
}) | |||
), | |||
state( | |||
"1", | |||
style({ | |||
opacity: 1, | |||
display: "block" | |||
}) | |||
), | |||
transition("0 => 1", animate("300ms")), | |||
transition("1 => 0", animate("300ms")) | |||
]) | |||
]; |
@ -0,0 +1,12 @@ | |||
<div class="breadcrumb-bar" *ngIf="layout.layoutConf.useBreadcrumb && layout.layoutConf.breadcrumb === 'simple'"> | |||
<ul class="breadcrumb"> | |||
<li *ngFor="let part of routeParts"><a routerLink="/{{part.url}}">{{part.breadcrumb}}</a></li> | |||
</ul> | |||
</div> | |||
<div class="breadcrumb-title" *ngIf="layout.layoutConf.useBreadcrumb && layout.layoutConf.breadcrumb === 'title'"> | |||
<h1 class="bc-title">{{routeParts[routeParts.length -1]?.breadcrumb}}</h1> | |||
<ul class="breadcrumb" *ngIf="routeParts.length > 1"> | |||
<li *ngFor="let part of routeParts"><a routerLink="/{{part.url}}" class="text-muted">{{part.breadcrumb}}</a></li> | |||
</ul> | |||
</div> |
@ -0,0 +1,66 @@ | |||
import { Component, OnInit, OnDestroy } from '@angular/core'; | |||
import { Router, NavigationEnd, ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; | |||
import { RoutePartsService } from '../../../shared/services/route-parts.service'; | |||
import { LayoutService } from '../../../shared/services/layout.service'; | |||
import { Subscription } from "rxjs"; | |||
import { filter } from 'rxjs/operators'; | |||
@Component({ | |||
selector: 'app-breadcrumb', | |||
templateUrl: './breadcrumb.component.html', | |||
styleUrls: ['./breadcrumb.component.scss'] | |||
}) | |||
export class BreadcrumbComponent implements OnInit, OnDestroy { | |||
routeParts:any[]; | |||
routerEventSub: Subscription; | |||
// public isEnabled: boolean = true; | |||
constructor( | |||
private router: Router, | |||
private routePartsService: RoutePartsService, | |||
private activeRoute: ActivatedRoute, | |||
public layout: LayoutService | |||
) { | |||
this.routeParts = this.routePartsService.generateRouteParts(this.activeRoute.snapshot); | |||
this.routerEventSub = this.router.events | |||
.pipe(filter(event => event instanceof NavigationEnd)) | |||
.subscribe((routeChange) => { | |||
this.routeParts = this.routePartsService.generateRouteParts(this.activeRoute.snapshot); | |||
// generate url from parts | |||
this.routeParts.reverse().map((item, i) => { | |||
item.breadcrumb = this.parseText(item); | |||
item.urlSegments.forEach((urlSegment, j) => { | |||
if(j === 0) | |||
return item.url = `${urlSegment.path}`; | |||
item.url += `/${urlSegment.path}` | |||
}); | |||
if(i === 0) { | |||
return item; | |||
} | |||
// prepend previous part to current part | |||
item.url = `${this.routeParts[i - 1].url}/${item.url}`; | |||
return item; | |||
}); | |||
}); | |||
} | |||
ngOnInit() { | |||
} | |||
ngOnDestroy() { | |||
if(this.routerEventSub) { | |||
this.routerEventSub.unsubscribe() | |||
} | |||
} | |||
parseText(part) { | |||
if(!part.breadcrumb) { | |||
return '' | |||
} | |||
part.breadcrumb = part.breadcrumb.replace(/{{([^{}]*)}}/g, function (a, b) { | |||
var r = part.params[b]; | |||
return typeof r === 'string' ? r : a; | |||
}); | |||
return part.breadcrumb; | |||
} | |||
} |
@ -0,0 +1,12 @@ | |||
<button mat-button [color]="color" class="button-loading {{btnClass}}" [type]="type" [disabled]="loading" | |||
[ngClass]="{ | |||
loading: loading, | |||
'mat-button': !raised, | |||
'mat-raised-button': raised | |||
}"> | |||
<div class="btn-spinner" *ngIf="loading"></div> | |||
<span *ngIf="!loading"> | |||
<ng-content></ng-content> | |||
</span> | |||
<span *ngIf="loading">{{loadingText}}</span> | |||
</button> |
@ -0,0 +1,25 @@ | |||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | |||
import { ButtonLoadingComponent } from './button-loading.component'; | |||
describe('ButtonLoadingComponent', () => { | |||
let component: ButtonLoadingComponent; | |||
let fixture: ComponentFixture<ButtonLoadingComponent>; | |||
beforeEach(async(() => { | |||
TestBed.configureTestingModule({ | |||
declarations: [ ButtonLoadingComponent ] | |||
}) | |||
.compileComponents(); | |||
})); | |||
beforeEach(() => { | |||
fixture = TestBed.createComponent(ButtonLoadingComponent); | |||
component = fixture.componentInstance; | |||
fixture.detectChanges(); | |||
}); | |||
it('should create', () => { | |||
expect(component).toBeTruthy(); | |||
}); | |||
}); |
@ -0,0 +1,23 @@ | |||
import { Component, OnInit, Input } from '@angular/core'; | |||
@Component({ | |||
selector: 'button-loading', | |||
templateUrl: './button-loading.component.html', | |||
styleUrls: ['./button-loading.component.scss'] | |||
}) | |||
export class ButtonLoadingComponent implements OnInit { | |||
@Input('loading') loading: boolean; | |||
@Input('btnClass') btnClass: string; | |||
@Input('raised') raised: boolean = true; | |||
@Input('loadingText') loadingText = 'Please wait'; | |||
@Input('type') type: 'button' | 'submit' = 'submit'; | |||
@Input('color') color: 'primary' | 'accent' | 'warn'; | |||
constructor() { | |||
} | |||
ngOnInit() { | |||
} | |||
} |
@ -0,0 +1,131 @@ | |||
<div class="handle" *ngIf="!isCustomizerOpen"> | |||
<button | |||
mat-fab | |||
color="primary" | |||
(click)="isCustomizerOpen = true"> | |||
<mat-icon class="spin text-white">settings</mat-icon> | |||
</button> | |||
</div> | |||
<div id="app-customizer" *ngIf="isCustomizerOpen"> | |||
<mat-card class="p-0"> | |||
<mat-card-title class="m-0 light-gray"> | |||
<div class="card-title-text" fxLayout="row wrap" fxLayoutAlign="center center"> | |||
<mat-button-toggle-group #viewMode="matButtonToggleGroup"> | |||
<mat-button-toggle matTooltip="Demos" value="demos" [checked]="true" aria-label="T"> | |||
<mat-icon>apps</mat-icon> | |||
</mat-button-toggle> | |||
<mat-button-toggle matTooltip="Settings" value="settings" aria-label=""> | |||
<mat-icon>settings</mat-icon> | |||
</mat-button-toggle> | |||
<mat-button-toggle matTooltip="JSON" value="json" aria-label=""> | |||
<mat-icon>code</mat-icon> | |||
</mat-button-toggle> | |||
</mat-button-toggle-group> | |||
<span fxFlex></span> | |||
<button | |||
class="card-control" | |||
mat-icon-button | |||
(click)="isCustomizerOpen = false"> | |||
<mat-icon>close</mat-icon> | |||
</button> | |||
</div> | |||
</mat-card-title> | |||
<mat-card-content *ngIf="viewMode.value === 'json'" style="min-height: 100vh"> | |||
<a class="text-primary" href="http://demos.ui-lib.com/matx-angular-doc/layout.html"><small>What is this?</small></a> | |||
<pre><code [matxHighlight]="this.layoutConf | json"></code></pre> | |||
</mat-card-content> | |||
<mat-card-content [perfectScrollbar] *ngIf="viewMode.value === 'demos'"> | |||
<div class="layout-boxes"> | |||
<div class="layout-box" *ngFor="let demo of customizer.layoutOptions" (click)="layout.publishLayoutChange(demo.options)"> | |||
<div> | |||
<span class="layout-name"> | |||
<button mat-raised-button color="accent"> | |||
{{demo.name}} | |||
</button> | |||
</span> | |||
<img [src]="demo.thumbnail" [alt]="demo.name"> | |||
</div> | |||
</div> | |||
</div> | |||
</mat-card-content> | |||
<mat-card-content [perfectScrollbar] *ngIf="viewMode.value === 'settings'"> | |||
<div class="pb-1 mb-1 border-bottom"> | |||
<h6 class="title text-muted">Header Colors</h6> | |||
<div class="colors"> | |||
<div | |||
class="color {{c.class}}" | |||
*ngFor="let c of customizer.topbarColors" | |||
(click)="customizer.changeTopbarColor(c)"> | |||
<mat-icon class="active-icon" *ngIf="c.active">check</mat-icon> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="pb-1 mb-1 border-bottom"> | |||
<h6 class="title text-muted">Sidebar colors</h6> | |||
<div class="colors"> | |||
<div | |||
class="color {{c.class}}" | |||
*ngFor="let c of customizer.sidebarColors" | |||
(click)="customizer.changeSidebarColor(c)"> | |||
<mat-icon class="active-icon" *ngIf="c.active">check</mat-icon> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="pb-1 mb-1 border-bottom"> | |||
<h6 class="title text-muted">Material Themes</h6> | |||
<div class="colors"> | |||
<div class="color" *ngFor="let theme of matxThemes" | |||
(click)="changeTheme(theme)" [style.background]="theme.baseColor"> | |||
<mat-icon class="active-icon" *ngIf="theme.isActive">check</mat-icon> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="pb-1 mb-1 border-bottom"> | |||
<h6 class="title text-muted">Footer Colors</h6> | |||
<div class="mb-1"> | |||
<mat-checkbox [(ngModel)]="isFooterFixed" (change)="layout.publishLayoutChange({ footerFixed: $event.checked })" [value]="selectedLayout !== 'top'">Fixed Footer</mat-checkbox> | |||
</div> | |||
<div class="colors"> | |||
<div | |||
class="color {{c.class}}" | |||
*ngFor="let c of customizer.footerColors" | |||
(click)="customizer.changeFooterColor(c)"> | |||
<mat-icon class="active-icon" *ngIf="c.active">check</mat-icon> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="pb-1 mb-1 border-bottom"> | |||
<h6 class="title text-muted">Breadcrumb</h6> | |||
<div class="mb-1"> | |||
<mat-checkbox [(ngModel)]="layoutConf.useBreadcrumb" (change)="toggleBreadcrumb($event)">Use breadcrumb</mat-checkbox> | |||
</div> | |||
<small class="text-muted">Breadcrumb types</small> | |||
<mat-radio-group fxLayout="column" [(ngModel)]="layoutConf.breadcrumb" [disabled]="!layoutConf.useBreadcrumb"> | |||
<mat-radio-button [value]="'simple'"> Simple </mat-radio-button> | |||
<mat-radio-button [value]="'title'"> Simple with title </mat-radio-button> | |||
</mat-radio-group> | |||
</div> | |||
<div class="pb-1 pos-rel mb-1 border-bottom"> | |||
<mat-checkbox [(ngModel)]="perfectScrollbarEnabled" (change)="tooglePerfectScrollbar($event)">Custom scrollbar</mat-checkbox> | |||
</div> | |||
</mat-card-content> | |||
</mat-card> | |||
</div> |
@ -0,0 +1,118 @@ | |||
.handle { | |||
position: fixed; | |||
bottom: 90px; | |||
right: 30px; | |||
z-index: 99; | |||
} | |||
#app-customizer { | |||
position: fixed; | |||
bottom: 0px; | |||
top: 0; | |||
right: 0; | |||
min-width: 180px; | |||
max-width: 280px; | |||
z-index: 999; | |||
.title { | |||
text-transform: uppercase; | |||
font-size: 12px; | |||
font-weight: bold; | |||
margin: 0 0 1rem; | |||
} | |||
.mat-card { | |||
margin: 0; | |||
border-radius: 0; | |||
} | |||
.mat-card-content { | |||
position: relative; | |||
padding: 1rem 1.5rem 2rem; | |||
height: calc(100vh - 120px); | |||
} | |||
} | |||
.pos-rel { | |||
position: relative; | |||
z-index: 99; | |||
.olay { | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
background: rgba(0, 0, 0, .5); | |||
z-index: 100; | |||
} | |||
} | |||
.colors { | |||
display: flex; | |||
flex-wrap: wrap; | |||
.color { | |||
position: relative; | |||
width: 36px; | |||
height: 36px; | |||
display: inline-block; | |||
border-radius: 50%; | |||
margin: 8px; | |||
text-align: center; | |||
box-shadow: 0 4px 20px 1px rgba(0,0,0,.06), 0 1px 4px rgba(0,0,0,.03); | |||
cursor: pointer; | |||
.active-icon { | |||
position: absolute; | |||
left: 0; | |||
right: 0; | |||
margin: auto; | |||
top: 6px; | |||
} | |||
} | |||
} | |||
.layout-box { | |||
width: 100%; | |||
margin: 16px 0; | |||
max-height: 150px; | |||
border-radius: 8px; | |||
overflow: hidden; | |||
cursor: pointer; | |||
> div { | |||
overflow: hidden; | |||
display: flex; | |||
position: relative; | |||
// height: 76px; | |||
width: 100%; | |||
&:hover { | |||
&::before, | |||
.layout-name { | |||
display: block; | |||
} | |||
} | |||
&::before, | |||
.layout-name { | |||
text-align: center; | |||
position: absolute; | |||
top: 0; | |||
left: 0; | |||
right: 0; | |||
display: none; | |||
} | |||
&::before { | |||
content: " "; | |||
width: 100%; | |||
height: 100%; | |||
background: rgba(0,0,0,0.3); | |||
} | |||
.layout-name { | |||
color: #ffffff; | |||
top: calc(50% - 18px) | |||
} | |||
img { | |||
// position: absolute; | |||
top: 0; | |||
left: 0; | |||
} | |||
} | |||
} | |||
// [dir="rtl"] { | |||
// .handle {} | |||
// #app-customizer { | |||
// right: auto; | |||
// left: 0; | |||
// } | |||
// } |
@ -0,0 +1,80 @@ | |||
import { Component, OnInit, Input, Renderer2 } from "@angular/core"; | |||
import { NavigationService } from "../../../shared/services/navigation.service"; | |||
import { LayoutService } from "../../../shared/services/layout.service"; | |||
import PerfectScrollbar from "perfect-scrollbar"; | |||
import { CustomizerService } from "app/shared/services/customizer.service"; | |||
import { ThemeService, ITheme } from "app/shared/services/theme.service"; | |||
@Component({ | |||
selector: "app-customizer", | |||
templateUrl: "./customizer.component.html", | |||
styleUrls: ["./customizer.component.scss"] | |||
}) | |||
export class CustomizerComponent implements OnInit { | |||
isCustomizerOpen: boolean = false; | |||
// viewMode: 'options' | 'json' | 'demos' = 'demos'; | |||
sidenavTypes = [ | |||
{ | |||
name: "Default Menu", | |||
value: "default-menu" | |||
}, | |||
{ | |||
name: "Separator Menu", | |||
value: "separator-menu" | |||
}, | |||
{ | |||
name: "Icon Menu", | |||
value: "icon-menu" | |||
} | |||
]; | |||
sidebarColors: any[]; | |||
topbarColors: any[]; | |||
layoutConf; | |||
selectedMenu: string = "icon-menu"; | |||
selectedLayout: string; | |||
isTopbarFixed = false; | |||
isFooterFixed = false; | |||
isRTL = false; | |||
matxThemes: ITheme[]; | |||
perfectScrollbarEnabled: boolean = true; | |||
constructor( | |||
private navService: NavigationService, | |||
public layout: LayoutService, | |||
private themeService: ThemeService, | |||
public customizer: CustomizerService, | |||
private renderer: Renderer2 | |||
) {} | |||
ngOnInit() { | |||
this.layoutConf = this.layout.layoutConf; | |||
this.selectedLayout = this.layoutConf.navigationPos; | |||
this.isTopbarFixed = this.layoutConf.topbarFixed; | |||
this.isRTL = this.layoutConf.dir === "rtl"; | |||
this.matxThemes = this.themeService.matxThemes; | |||
} | |||
changeTheme(theme) { | |||
// this.themeService.changeTheme(theme); | |||
this.layout.publishLayoutChange({matTheme: theme.name}) | |||
} | |||
changeLayoutStyle(data) { | |||
this.layout.publishLayoutChange({ navigationPos: this.selectedLayout }); | |||
} | |||
changeSidenav(data) { | |||
this.navService.publishNavigationChange(data.value); | |||
} | |||
toggleBreadcrumb(data) { | |||
this.layout.publishLayoutChange({ useBreadcrumb: data.checked }); | |||
} | |||
toggleTopbarFixed(data) { | |||
this.layout.publishLayoutChange({ topbarFixed: data.checked }); | |||
} | |||
toggleDir(data) { | |||
} | |||
tooglePerfectScrollbar(data) { | |||
this.layout.publishLayoutChange({perfectScrollbar: this.perfectScrollbarEnabled}) | |||
} | |||
} |
@ -0,0 +1,6 @@ | |||
<footer class="main-footer"> | |||
<a mat-raised-button color="primary" href="https://ui-lib.com/matx-angular-dashboard" class="mr-2">Download Free version</a> | |||
<a mat-raised-button color="accent" href="https://ui-lib.com/matx-angular-dashboard-pro">Upgrade to Pro</a> | |||
<span class="m-auto"></span> | |||
Design & Developed by <a href="https://ui-lib.com">UI Lib</a> | |||
</footer> |
@ -0,0 +1,25 @@ | |||
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; | |||
import { FooterComponent } from './footer.component'; | |||
describe('FooterComponent', () => { | |||
let component: FooterComponent; | |||
let fixture: ComponentFixture<FooterComponent>; | |||
beforeEach(async(() => { | |||
TestBed.configureTestingModule({ | |||
declarations: [ FooterComponent ] | |||
}) | |||
.compileComponents(); | |||
})); | |||
beforeEach(() => { | |||
fixture = TestBed.createComponent(FooterComponent); | |||
component = fixture.componentInstance; | |||
fixture.detectChanges(); | |||
}); | |||
it('should create', () => { | |||
expect(component).toBeTruthy(); | |||
}); | |||
}); |
@ -0,0 +1,15 @@ | |||
import { Component, OnInit } from '@angular/core'; | |||
@Component({ | |||
selector: 'app-footer', | |||
templateUrl: './footer.component.html', | |||
styleUrls: ['./footer.component.scss'] | |||
}) | |||
export class FooterComponent implements OnInit { | |||
constructor() { } | |||
ngOnInit() { | |||
} | |||
} |
@ -0,0 +1,75 @@ | |||
import { Component, OnInit, EventEmitter, Input, Output, Renderer2 } from '@angular/core'; | |||
import { ThemeService } from '../../services/theme.service'; | |||
import { LayoutService } from '../../services/layout.service'; | |||
import { JwtAuthService } from 'app/shared/services/auth/jwt-auth.service'; | |||
@Component({ | |||
selector: 'app-header-side', | |||
templateUrl: './header-side.template.html' | |||
}) | |||
export class HeaderSideComponent implements OnInit { | |||
@Input() notificPanel; | |||
public availableLangs = [{ | |||
name: 'EN', | |||
code: 'en', | |||
flag: 'flag-icon-us' | |||
}, { | |||
name: 'ES', | |||
code: 'es', | |||
flag: 'flag-icon-es' | |||
}] | |||
currentLang = this.availableLangs[0]; | |||
public matxThemes; | |||
public layoutConf:any; | |||
constructor( | |||
private themeService: ThemeService, | |||
private layout: LayoutService, | |||
private renderer: Renderer2, | |||
public jwtAuth: JwtAuthService | |||
) {} | |||
ngOnInit() { | |||
this.matxThemes = this.themeService.matxThemes; | |||
this.layoutConf = this.layout.layoutConf; | |||
} | |||
setLang(lng) { | |||
} | |||
changeTheme(theme) { | |||
// this.themeService.changeTheme(theme); | |||
} | |||
toggleNotific() { | |||
this.notificPanel.toggle(); | |||
} | |||
toggleSidenav() { | |||
if(this.layoutConf.sidebarStyle === 'closed') { | |||
return this.layout.publishLayoutChange({ | |||
sidebarStyle: 'full' | |||
}) | |||
} | |||
this.layout.publishLayoutChange({ | |||
sidebarStyle: 'closed' | |||
}) | |||
} | |||
toggleCollapse() { | |||
// compact --> full | |||
if(this.layoutConf.sidebarStyle === 'compact') { | |||
return this.layout.publishLayoutChange({ | |||
sidebarStyle: 'full', | |||
sidebarCompactToggle: false | |||
}, {transitionClass: true}) | |||
} | |||
// * --> compact | |||
this.layout.publishLayoutChange({ | |||
sidebarStyle: 'compact', | |||
sidebarCompactToggle: true | |||
}, {transitionClass: true}) | |||
} | |||
onSearch(e) { | |||
// console.log(e) | |||
} | |||
} |
@ -0,0 +1,77 @@ | |||
<mat-toolbar class="topbar"> | |||
<!-- Sidenav toggle button --> | |||
<!-- {{layoutConf.sidebarStyle}} | |||
<button | |||
*ngIf="layoutConf.sidebarStyle !== 'compact'" | |||
mat-icon-button | |||
id="sidenavToggle" | |||
fxHide.gt-md | |||
fxHide.md | |||
(click)="toggleSidenav()" | |||
matTooltip="Toggle Hide/Open" | |||
> | |||
<mat-icon>menu</mat-icon> | |||
</button> --> | |||
<button | |||
*ngIf="layoutConf.sidebarStyle !== 'compact'" | |||
mat-icon-button | |||
id="sidenavToggle" | |||
(click)="toggleSidenav()" | |||
matTooltip="Toggle Hide/Open" | |||
> | |||
<mat-icon>menu</mat-icon> | |||
</button> | |||
<!-- Search form --> | |||
<!-- <div fxFlex fxHide.lt-sm="true" class="search-bar"> | |||
<form class="top-search-form"> | |||
<mat-icon role="img">search</mat-icon> | |||
<input autofocus="true" placeholder="Search" type="text" /> | |||
</form> | |||
</div> --> | |||
<span fxFlex></span> | |||
<matx-search-input-over placeholder="Country (e.g. US)" resultPage="/search"> | |||
</matx-search-input-over> | |||
<!-- Open "views/search-view/result-page.component" to understand how to subscribe to input field value --> | |||
<!-- Notification toggle button --> | |||
<button | |||
mat-icon-button | |||
matTooltip="Notifications" | |||
(click)="toggleNotific()" | |||
[style.overflow]="'visible'" | |||
class="topbar-button-right" | |||
> | |||
<mat-icon>notifications</mat-icon> | |||
<span class="notification-number mat-bg-warn">3</span> | |||
</button> | |||
<!-- Top left user menu --> | |||
<button | |||
mat-icon-button | |||
[matMenuTriggerFor]="accountMenu" | |||
class="topbar-button-right img-button" | |||
> | |||
<img src="assets/images/face-7.jpg" alt="" /> | |||
</button> | |||
<mat-menu #accountMenu="matMenu"> | |||
<button mat-menu-item [routerLink]="['/profile/overview']"> | |||
<mat-icon>account_box</mat-icon> | |||
<span>Profile</span> | |||
</button> | |||
<button mat-menu-item [routerLink]="['/profile/settings']"> | |||
<mat-icon>settings</mat-icon> | |||
<span>Account Settings</span> | |||
</button> | |||
<button mat-menu-item> | |||
<mat-icon>notifications_off</mat-icon> | |||
<span>Disable alerts</span> | |||
</button> | |||
<button mat-menu-item (click)="jwtAuth.signout()"> | |||
<mat-icon>exit_to_app</mat-icon> | |||
<span>Sign out</span> | |||
</button> | |||
</mat-menu> | |||
</mat-toolbar> |
@ -0,0 +1,138 @@ | |||
import { Component, OnInit, AfterViewInit, ViewChild, HostListener, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; | |||
import { | |||
Router, | |||
NavigationEnd, | |||
RouteConfigLoadStart, | |||
RouteConfigLoadEnd, | |||
ResolveStart, | |||
ResolveEnd | |||
} from '@angular/router'; | |||
import { Subscription } from "rxjs"; | |||
import { ThemeService } from '../../../services/theme.service'; | |||
import { LayoutService } from '../../../services/layout.service'; | |||
import { filter } from 'rxjs/operators'; | |||
import { JwtAuthService } from '../../../services/auth/jwt-auth.service'; | |||
@Component({ | |||
selector: 'app-admin-layout', | |||
templateUrl: './admin-layout.template.html', | |||
// changeDetection: ChangeDetectionStrategy.OnPush | |||
}) | |||
export class AdminLayoutComponent implements OnInit, AfterViewInit { | |||
public isModuleLoading: Boolean = false; | |||
private moduleLoaderSub: Subscription; | |||
private layoutConfSub: Subscription; | |||
private routerEventSub: Subscription; | |||
public scrollConfig = {} | |||
public layoutConf: any = {}; | |||
public adminContainerClasses: any = {}; | |||
constructor( | |||
private router: Router, | |||
public themeService: ThemeService, | |||
private layout: LayoutService, | |||
private cdr: ChangeDetectorRef, | |||
private jwtAuth: JwtAuthService | |||
) { | |||
// Check Auth Token is valid | |||
this.jwtAuth.checkTokenIsValid().subscribe(); | |||
// Close sidenav after route change in mobile | |||
this.routerEventSub = router.events.pipe(filter(event => event instanceof NavigationEnd)) | |||
.subscribe((routeChange: NavigationEnd) => { | |||
this.layout.adjustLayout({ route: routeChange.url }); | |||
this.scrollToTop(); | |||
}); | |||
} | |||
ngOnInit() { | |||
// this.layoutConf = this.layout.layoutConf; | |||
this.layoutConfSub = this.layout.layoutConf$.subscribe((layoutConf) => { | |||
this.layoutConf = layoutConf; | |||
// console.log(this.layoutConf); | |||
this.adminContainerClasses = this.updateAdminContainerClasses(this.layoutConf); | |||
this.cdr.markForCheck(); | |||
}); | |||
// FOR MODULE LOADER FLAG | |||
this.moduleLoaderSub = this.router.events.subscribe(event => { | |||
if(event instanceof RouteConfigLoadStart || event instanceof ResolveStart) { | |||
this.isModuleLoading = true; | |||
} | |||
if(event instanceof RouteConfigLoadEnd || event instanceof ResolveEnd) { | |||
this.isModuleLoading = false; | |||
} | |||
}); | |||
} | |||
@HostListener('window:resize', ['$event']) | |||
onResize(event) { | |||
this.layout.adjustLayout(event); | |||
} | |||
ngAfterViewInit() { | |||
} | |||
scrollToTop() { | |||
if(document) { | |||
setTimeout(() => { | |||
let element; | |||
if(this.layoutConf.topbarFixed) { | |||
element = <HTMLElement>document.querySelector('#rightside-content-hold'); | |||
} else { | |||
element = <HTMLElement>document.querySelector('#main-content-wrap'); | |||
} | |||
element.scrollTop = 0; | |||
}) | |||
} | |||
} | |||
ngOnDestroy() { | |||
if(this.moduleLoaderSub) { | |||
this.moduleLoaderSub.unsubscribe(); | |||
} | |||
if(this.layoutConfSub) { | |||
this.layoutConfSub.unsubscribe(); | |||
} | |||
if(this.routerEventSub) { | |||
this.routerEventSub.unsubscribe(); | |||
} | |||
} | |||
closeSidebar() { | |||
this.layout.publishLayoutChange({ | |||
sidebarStyle: 'closed' | |||
}) | |||
} | |||
sidebarMouseenter(e) { | |||
// console.log(this.layoutConf); | |||
if(this.layoutConf.sidebarStyle === 'compact') { | |||
this.layout.publishLayoutChange({sidebarStyle: 'full'}, {transitionClass: true}); | |||
} | |||
} | |||
sidebarMouseleave(e) { | |||
// console.log(this.layoutConf); | |||
if ( | |||
this.layoutConf.sidebarStyle === 'full' && | |||
this.layoutConf.sidebarCompactToggle | |||
) { | |||
this.layout.publishLayoutChange({sidebarStyle: 'compact'}, {transitionClass: true}); | |||
} | |||
} | |||
updateAdminContainerClasses(layoutConf) { | |||
return { | |||
'navigation-top': layoutConf.navigationPos === 'top', | |||
'sidebar-full': layoutConf.sidebarStyle === 'full', | |||
'sidebar-compact': layoutConf.sidebarStyle === 'compact' && layoutConf.navigationPos === 'side', | |||
'compact-toggle-active': layoutConf.sidebarCompactToggle, | |||
'sidebar-compact-big': layoutConf.sidebarStyle === 'compact-big' && layoutConf.navigationPos === 'side', | |||
'sidebar-opened': layoutConf.sidebarStyle !== 'closed' && layoutConf.navigationPos === 'side', | |||
'sidebar-closed': layoutConf.sidebarStyle === 'closed', | |||
'fixed-topbar': layoutConf.topbarFixed && layoutConf.navigationPos === 'side' | |||
} | |||
} | |||
} |
@ -0,0 +1,66 @@ | |||
<div class="app-admin-wrap" dir='ltr'> | |||
<!-- Main Container --> | |||
<mat-sidenav-container | |||
[dir]='layoutConf.dir' | |||
class="app-admin-container app-side-nav-container mat-drawer-transition sidebar-{{layoutConf?.sidebarColor}} topbar-{{layoutConf?.topbarColor}} footer-{{layoutConf?.footerColor}}" | |||
[ngClass]="adminContainerClasses"> | |||
<mat-sidenav-content> | |||
<!-- SIDEBAR --> | |||
<!-- ONLY REQUIRED FOR **SIDE** NAVIGATION LAYOUT --> | |||
<app-sidebar-side | |||
*ngIf="layoutConf.navigationPos === 'side'" | |||
(mouseenter)="sidebarMouseenter($event)" | |||
(mouseleave)="sidebarMouseleave($event)" | |||
></app-sidebar-side> | |||
<!-- App content --> | |||
<div class="main-content-wrap" id="main-content-wrap" [perfectScrollbar]="" [disabled]="layoutConf.topbarFixed || !layoutConf.perfectScrollbar"> | |||
<!-- Header for side navigation layout --> | |||
<!-- ONLY REQUIRED FOR **SIDE** NAVIGATION LAYOUT --> | |||
<app-header-side | |||
*ngIf="layoutConf.navigationPos === 'side'" | |||
[notificPanel]="notificationPanel"> | |||
</app-header-side> | |||
<div class="rightside-content-hold" id="rightside-content-hold" [perfectScrollbar]="scrollConfig" [disabled]="!layoutConf.topbarFixed || !layoutConf.perfectScrollbar"> | |||
<!-- View Loader --> | |||
<div class="view-loader" *ngIf="isModuleLoading" style="position:fixed;" | |||
fxLayout="column" fxLayoutAlign="center center"> | |||
<div class="spinner"> | |||
<div class="double-bounce1 mat-bg-accent"></div> | |||
<div class="double-bounce2 mat-bg-primary"></div> | |||
</div> | |||
</div> | |||
<!-- Breadcrumb --> | |||
<app-breadcrumb></app-breadcrumb> | |||
<!-- View outlet --> | |||
<router-outlet></router-outlet> | |||
<span class="m-auto" *ngIf="!layoutConf.footerFixed"></span> | |||
<app-footer *ngIf="!layoutConf.footerFixed" style="display: block; margin: 0 -.333rem -.33rem"></app-footer> | |||
</div> | |||
<span class="m-auto" *ngIf="layoutConf.footerFixed"></span> | |||
<app-footer *ngIf="layoutConf.footerFixed"></app-footer> | |||
</div> | |||
<!-- View overlay for mobile navigation --> | |||
<div class="sidebar-backdrop" | |||
[ngClass]="{'visible': layoutConf.sidebarStyle !== 'closed' && layoutConf.isMobile}" | |||
(click)="closeSidebar()"></div> | |||
</mat-sidenav-content> | |||
<!-- Notificaation bar --> | |||
<mat-sidenav #notificationPanel mode="over" class="" position="end"> | |||
<div class="nofication-panel" fxLayout="column"> | |||
<app-notifications [notificPanel]="notificationPanel"></app-notifications> | |||
</div> | |||
</mat-sidenav> | |||
</mat-sidenav-container> | |||
</div> | |||
<!-- Only for demo purpose --> | |||
<!-- Remove this from your production version --> | |||
<app-customizer></app-customizer> |
@ -0,0 +1 @@ | |||
<router-outlet></router-outlet> |
@ -0,0 +1,14 @@ | |||
import { Component, OnInit } from '@angular/core'; | |||
@Component({ | |||
selector: 'app-auth-layout', | |||
templateUrl: './auth-layout.component.html' | |||
}) | |||
export class AuthLayoutComponent implements OnInit { | |||
constructor() { } | |||
ngOnInit() { | |||
} | |||
} |
@ -0,0 +1,18 @@ | |||
<div class="text-center mat-bg-primary pt-1 pb-1"> | |||
<h6 class="m-0">Notifications</h6> | |||
</div> | |||
<mat-nav-list class="notification-list" role="list"> | |||
<!-- Notification item --> | |||
<mat-list-item *ngFor="let n of notifications" class="notific-item" role="listitem" routerLinkActive="open"> | |||
<mat-icon [color]="n.color" class="notific-icon mr-1">{{n.icon}}</mat-icon> | |||
<a [routerLink]="[n.route || '/dashboard']"> | |||
<div class="mat-list-text"> | |||
<h4 class="message">{{n.message}}</h4> | |||
<small class="time text-muted">{{n.time}}</small> | |||
</div> | |||
</a> | |||
</mat-list-item> | |||
</mat-nav-list> | |||
<div class="text-center mt-1" *ngIf="notifications.length"> | |||
<small><a href="#" (click)="clearAll($event)">Clear all notifications</a></small> | |||
</div> |
@ -0,0 +1,46 @@ | |||
import { Component, OnInit, ViewChild, Input } from '@angular/core'; | |||
import { MatSidenav } from '@angular/material/sidenav'; | |||
import { Router, NavigationEnd } from '@angular/router'; | |||
@Component({ | |||
selector: 'app-notifications', | |||
templateUrl: './notifications.component.html' | |||
}) | |||
export class NotificationsComponent implements OnInit { | |||
@Input() notificPanel; | |||
// Dummy notifications | |||
notifications = [{ | |||
message: 'New contact added', | |||
icon: 'assignment_ind', | |||
time: '1 min ago', | |||
route: '/inbox', | |||
color: 'primary' | |||
}, { | |||
message: 'New message', | |||
icon: 'chat', | |||
time: '4 min ago', | |||
route: '/chat', | |||
color: 'accent' | |||
}, { | |||
message: 'Server rebooted', | |||
icon: 'settings_backup_restore', | |||
time: '12 min ago', | |||
route: '/charts', | |||
color: 'warn' | |||
}] | |||
constructor(private router: Router) {} | |||
ngOnInit() { | |||
this.router.events.subscribe((routeChange) => { | |||
if (routeChange instanceof NavigationEnd) { | |||
this.notificPanel.close(); | |||
} | |||
}); | |||
} | |||
clearAll(e) { | |||
e.preventDefault(); | |||
this.notifications = []; | |||
} | |||
} |
@ -0,0 +1,66 @@ | |||
import { NgModule } from "@angular/core"; | |||
import { RouterModule } from "@angular/router"; | |||
import { SharedMaterialModule } from "../shared-material.module"; | |||
import { CommonModule } from "@angular/common"; | |||
import { FormsModule } from "@angular/forms"; | |||
import { PerfectScrollbarModule } from "ngx-perfect-scrollbar"; | |||
import { SearchModule } from "../search/search.module"; | |||
import { SharedPipesModule } from "../pipes/shared-pipes.module"; | |||
import { FlexLayoutModule } from "@angular/flex-layout"; | |||
import { SharedDirectivesModule } from "../directives/shared-directives.module"; | |||
// ONLY REQUIRED FOR **SIDE** NAVIGATION LAYOUT | |||
import { HeaderSideComponent } from "./header-side/header-side.component"; | |||
import { SidebarSideComponent } from "./sidebar-side/sidebar-side.component"; | |||
// ONLY FOR DEMO | |||
import { CustomizerComponent } from "./customizer/customizer.component"; | |||
// ALWAYS REQUIRED | |||
import { AdminLayoutComponent } from "./layouts/admin-layout/admin-layout.component"; | |||
import { AuthLayoutComponent } from "./layouts/auth-layout/auth-layout.component"; | |||
import { NotificationsComponent } from "./notifications/notifications.component"; | |||
import { SidenavComponent } from "./sidenav/sidenav.component"; | |||
import { FooterComponent } from "./footer/footer.component"; | |||
import { BreadcrumbComponent } from "./breadcrumb/breadcrumb.component"; | |||
import { AppComfirmComponent } from "../services/app-confirm/app-confirm.component"; | |||
import { AppLoaderComponent } from "../services/app-loader/app-loader.component"; | |||
import { ButtonLoadingComponent } from "./button-loading/button-loading.component"; | |||
const components = [ | |||
SidenavComponent, | |||
NotificationsComponent, | |||
SidebarSideComponent, | |||
HeaderSideComponent, | |||
AdminLayoutComponent, | |||
AuthLayoutComponent, | |||
BreadcrumbComponent, | |||
AppComfirmComponent, | |||
AppLoaderComponent, | |||
ButtonLoadingComponent, | |||
CustomizerComponent, | |||
FooterComponent, | |||
]; | |||
@NgModule({ | |||
imports: [ | |||
CommonModule, | |||
FormsModule, | |||
RouterModule, | |||
FlexLayoutModule, | |||
PerfectScrollbarModule, | |||
SearchModule, | |||
SharedPipesModule, | |||
SharedDirectivesModule, | |||
SharedMaterialModule | |||
], | |||
declarations: components, | |||
entryComponents: [ | |||
AppComfirmComponent, | |||
AppLoaderComponent | |||
], | |||
exports: components | |||
}) | |||
export class SharedComponentsModule {} |
@ -0,0 +1,95 @@ | |||
<div class="sidebar-panel"> | |||
<div | |||
id="scroll-area" | |||
[perfectScrollbar] | |||
class="navigation-hold" | |||
fxLayout="column" | |||
> | |||
<div class="sidebar-hold"> | |||
<!-- App Logo --> | |||
<div class="branding px-20"> | |||
<img src="assets/images/logo.png" alt="" class="app-logo" /> | |||
<span class="app-logo-text">MatX</span> | |||
<span | |||
style="margin: auto" | |||
*ngIf="layoutConf.sidebarStyle !== 'compact'" | |||
></span> | |||
<!-- <mat-slide-toggle | |||
fxHide.lt-md | |||
(change)="toggleCollapse()" | |||
[checked]="!layoutConf.sidebarCompactToggle" | |||
*ngIf="layoutConf.sidebarStyle !== 'compact'" | |||
></mat-slide-toggle> --> | |||
</div> | |||
<!-- Sidebar user --> | |||
<div class="app-user"> | |||
<div class="app-user-photo"> | |||
<img src="assets/images/face-7.jpg" class="mat-elevation-z1" alt="" /> | |||
</div> | |||
<div class="ml-16"> | |||
<span class="app-user-name mb-4"> | |||
Watson Joyce | |||
</span> | |||
<!-- Small buttons --> | |||
<div class="app-user-controls"> | |||
<button | |||
class="text-muted" | |||
mat-icon-button | |||
mat-xs-button | |||
[matMenuTriggerFor]="appUserMenu" | |||
> | |||
<mat-icon>settings</mat-icon> | |||
</button> | |||
<button | |||
class="text-muted" | |||
mat-icon-button | |||
mat-xs-button | |||
matTooltip="Inbox" | |||
routerLink="/inbox" | |||
> | |||
<mat-icon>email</mat-icon> | |||
</button> | |||
<button | |||
class="text-muted" | |||
mat-icon-button | |||
mat-xs-button | |||
matTooltip="Sign Out" | |||
(click)="jwtAuth.signout()" | |||
> | |||
<mat-icon>exit_to_app</mat-icon> | |||
</button> | |||
<mat-menu #appUserMenu="matMenu"> | |||
<button mat-menu-item routerLink="/profile/overview"> | |||
<mat-icon>account_box</mat-icon> | |||
<span>Profile</span> | |||
</button> | |||
<button mat-menu-item routerLink="/profile/settings"> | |||
<mat-icon>settings</mat-icon> | |||
<span>Account Settings</span> | |||
</button> | |||
<button mat-menu-item routerLink="/calendar"> | |||
<mat-icon>date_range</mat-icon> | |||
<span>Calendar</span> | |||
</button> | |||
<button mat-menu-item (click)="jwtAuth.signout()"> | |||
<mat-icon>exit_to_app</mat-icon> | |||
<span>Sign out</span> | |||
</button> | |||
</mat-menu> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- Navigation --> | |||
<app-sidenav | |||
[items]="menuItems" | |||
[hasIconMenu]="hasIconTypeMenuItem" | |||
[iconMenuTitle]="iconTypeMenuTitle" | |||
></app-sidenav> | |||
</div> | |||
</div> | |||
</div> |
@ -0,0 +1,48 @@ | |||
import { Component, OnInit, OnDestroy, AfterViewInit } from "@angular/core"; | |||
import { NavigationService } from "../../../shared/services/navigation.service"; | |||
import { ThemeService } from "../../services/theme.service"; | |||
import { Subscription } from "rxjs"; | |||
import { ILayoutConf, LayoutService } from "app/shared/services/layout.service"; | |||
import { JwtAuthService } from "app/shared/services/auth/jwt-auth.service"; | |||
@Component({ | |||
selector: "app-sidebar-side", | |||
templateUrl: "./sidebar-side.component.html" | |||
}) | |||
export class SidebarSideComponent implements OnInit, OnDestroy, AfterViewInit { | |||
public menuItems: any[]; | |||
public hasIconTypeMenuItem: boolean; | |||
public iconTypeMenuTitle: string; | |||
private menuItemsSub: Subscription; | |||
public layoutConf: ILayoutConf; | |||
constructor( | |||
private navService: NavigationService, | |||
public themeService: ThemeService, | |||
private layout: LayoutService, | |||
public jwtAuth: JwtAuthService | |||
) {} | |||
ngOnInit() { | |||
this.iconTypeMenuTitle = this.navService.iconTypeMenuTitle; | |||
this.menuItemsSub = this.navService.menuItems$.subscribe(menuItem => { | |||
this.menuItems = menuItem; | |||
//Checks item list has any icon type. | |||
this.hasIconTypeMenuItem = !!this.menuItems.filter( | |||
item => item.type === "icon" | |||
).length; | |||
}); | |||
this.layoutConf = this.layout.layoutConf; | |||
} | |||
ngAfterViewInit() {} | |||
ngOnDestroy() { | |||
if (this.menuItemsSub) { | |||
this.menuItemsSub.unsubscribe(); | |||
} | |||
} | |||
toggleCollapse() { | |||
this.layout.publishLayoutChange({ | |||
sidebarCompactToggle: !this.layoutConf.sidebarCompactToggle | |||
}); | |||
} | |||
} |
@ -0,0 +1,29 @@ | |||
import { Component, OnInit, Input } from "@angular/core"; | |||
@Component({ | |||
selector: "app-sidenav", | |||
templateUrl: "./sidenav.template.html" | |||
}) | |||
export class SidenavComponent { | |||
@Input("items") public menuItems: any[] = []; | |||
@Input("hasIconMenu") public hasIconTypeMenuItem: boolean; | |||
@Input("iconMenuTitle") public iconTypeMenuTitle: string; | |||
constructor() {} | |||
ngOnInit() {} | |||
// Only for demo purpose | |||
addMenuItem() { | |||
this.menuItems.push({ | |||
name: "ITEM", | |||
type: "dropDown", | |||
tooltip: "Item", | |||
icon: "done", | |||
state: "material", | |||
sub: [ | |||
{ name: "SUBITEM", state: "cards" }, | |||
{ name: "SUBITEM", state: "buttons" } | |||
] | |||
}); | |||
} | |||
} |
@ -0,0 +1,133 @@ | |||
<div class="sidenav-hold"> | |||
<ul appDropdown class="sidenav"> | |||
<li *ngFor="let item of menuItems" appDropdownLink routerLinkActive="open"> | |||
<div class="nav-item-sep" *ngIf="item.type === 'separator'"> | |||
<span class="text-muted">{{item.name}}</span> | |||
</div> | |||
<div | |||
*ngIf="!item.disabled && item.type !== 'separator' && item.type !== 'icon'" | |||
class="lvl1" | |||
> | |||
<a | |||
routerLink="/{{item.state}}" | |||
appDropdownToggle | |||
matRipple | |||
*ngIf="item.type === 'link'" | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<mat-icon>{{item.icon}}</mat-icon> | |||
<span class="item-name lvl1">{{item.name}}</span> | |||
<span fxFlex></span> | |||
<span | |||
class="menuitem-badge mat-bg-{{ badge.color }}" | |||
[ngStyle]="{background: badge.color}" | |||
*ngFor="let badge of item.badges" | |||
>{{ badge.value }}</span | |||
> | |||
</a> | |||
<a | |||
[href]="item.state" | |||
appDropdownToggle | |||
matRipple | |||
*ngIf="item.type === 'extLink'" | |||
target="_blank" | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<mat-icon>{{item.icon}}</mat-icon> | |||
<span class="item-name lvl1">{{item.name}}</span> | |||
<span fxFlex></span> | |||
<span | |||
class="menuitem-badge mat-bg-{{ badge.color }}" | |||
[ngStyle]="{background: badge.color}" | |||
*ngFor="let badge of item.badges" | |||
>{{ badge.value }}</span | |||
> | |||
</a> | |||
<!-- DropDown --> | |||
<a | |||
*ngIf="item.type === 'dropDown'" | |||
appDropdownToggle | |||
matRipple | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<mat-icon>{{item.icon}}</mat-icon> | |||
<span class="item-name lvl1">{{item.name}}</span> | |||
<span fxFlex></span> | |||
<span | |||
class="menuitem-badge mat-bg-{{ badge.color }}" | |||
[ngStyle]="{background: badge.color}" | |||
*ngFor="let badge of item.badges" | |||
>{{ badge.value }}</span | |||
> | |||
<mat-icon class="menu-caret">keyboard_arrow_right</mat-icon> | |||
</a> | |||
<!-- LEVEL 2 --> | |||
<ul class="submenu lvl2" appDropdown *ngIf="item.type === 'dropDown'"> | |||
<li | |||
*ngFor="let itemLvL2 of item.sub" | |||
appDropdownLink | |||
routerLinkActive="open" | |||
> | |||
<a | |||
routerLink="{{item.state ? '/'+item.state : ''}}/{{itemLvL2.state}}" | |||
appDropdownToggle | |||
*ngIf="itemLvL2.type !== 'dropDown' && itemLvL2.type !== 'extLink' " | |||
matRipple | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<span class="item-name lvl2">{{itemLvL2.name}}</span> | |||
<span fxFlex></span> | |||
</a> | |||
<a | |||
[href]="itemLvL2.state" | |||
appDropdownToggle | |||
matRipple | |||
*ngIf="itemLvL2.type === 'extLink'" | |||
target="_blank" | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<span class="item-name lvl2">{{itemLvL2.name}}</span> | |||
</a> | |||
<a | |||
*ngIf="itemLvL2.type === 'dropDown'" | |||
appDropdownToggle | |||
matRipple | |||
routerLinkActive="mat-bg-accent mat-elevation-z1" | |||
> | |||
<span class="item-name lvl2">{{itemLvL2.name}}</span> | |||
<span fxFlex></span> | |||
<mat-icon class="menu-caret">keyboard_arrow_right</mat-icon> | |||
</a> | |||
<!-- LEVEL 3 --> | |||
<ul | |||
class="submenu lvl3" | |||
appDropdown | |||
*ngIf="itemLvL2.type === 'dropDown'" | |||
> | |||
<li | |||
*ngFor="let itemLvL3 of itemLvL2.sub" | |||
appDropdownLink | |||
routerLinkActive="open" | |||
> | |||
<a | |||
routerLink="{{item.state ? '/'+item.state : ''}}{{itemLvL2.state ? '/'+itemLvL2.state : ''}}/{{itemLvL3.state}}" | |||
appDropdownToggle | |||
matRipple | |||
routerLinkActive="border-radius-4 mat-bg-accent mat-elevation-z1" | |||
> | |||
<span class="item-name lvl3" | |||
>{{itemLvL3.name}}</span | |||
> | |||
</a> | |||
</li> | |||
</ul> | |||
</li> | |||
</ul> | |||
</div> | |||
</li> | |||
</ul> | |||
</div> |
@ -0,0 +1,19 @@ | |||
import { Directive, HostListener, Inject } from '@angular/core'; | |||
import { DropdownLinkDirective } from './dropdown-link.directive'; | |||
@Directive({ | |||
selector: '[appDropdownToggle]' | |||
}) | |||
export class DropdownAnchorDirective { | |||
protected navlink: DropdownLinkDirective; | |||
constructor( @Inject(DropdownLinkDirective) navlink: DropdownLinkDirective) { | |||
this.navlink = navlink; | |||
} | |||
@HostListener('click', ['$event']) | |||
onClick(e: any) { | |||
this.navlink.toggle(); | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
import { | |||
Directive, HostBinding, Inject, Input, OnInit, OnDestroy | |||
} from '@angular/core'; | |||
import { AppDropdownDirective } from './dropdown.directive'; | |||
@Directive({ | |||
selector: '[appDropdownLink]' | |||
}) | |||
export class DropdownLinkDirective { | |||
@Input() public group: any; | |||
@HostBinding('class.open') | |||
@Input() | |||
get open(): boolean { | |||
return this._open; | |||
} | |||
set open(value: boolean) { | |||
this._open = value; | |||
if (value) { | |||
this.nav.closeOtherLinks(this); | |||
} | |||
} | |||
protected _open: boolean; | |||
protected nav: AppDropdownDirective; | |||
public constructor(@Inject(AppDropdownDirective) nav: AppDropdownDirective) { | |||
this.nav = nav; | |||
} | |||
public ngOnInit(): any { | |||
this.nav.addLink(this); | |||
} | |||
public ngOnDestroy(): any { | |||
this.nav.removeGroup(this); | |||
} | |||
public toggle(): any { | |||
this.open = !this.open; | |||
} | |||
} |
@ -0,0 +1,55 @@ | |||
import { Directive } from '@angular/core'; | |||
import { Router, NavigationEnd } from '@angular/router'; | |||
import { DropdownLinkDirective } from './dropdown-link.directive'; | |||
import { Subscription } from 'rxjs'; | |||
import { filter } from 'rxjs/operators'; | |||
@Directive({ | |||
selector: '[appDropdown]' | |||
}) | |||
export class AppDropdownDirective { | |||
protected navlinks: Array<DropdownLinkDirective> = []; | |||
private _router: Subscription; | |||
public closeOtherLinks(openLink: DropdownLinkDirective): void { | |||
this.navlinks.forEach((link: DropdownLinkDirective) => { | |||
if (link !== openLink) { | |||
link.open = false; | |||
} | |||
}); | |||
} | |||
public addLink(link: DropdownLinkDirective): void { | |||
this.navlinks.push(link); | |||
} | |||
public removeGroup(link: DropdownLinkDirective): void { | |||
const index = this.navlinks.indexOf(link); | |||
if (index !== -1) { | |||
this.navlinks.splice(index, 1); | |||
} | |||
} | |||
public getUrl() { | |||
return this.router.url; | |||
} | |||
public ngOnInit(): any { | |||
this._router = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: NavigationEnd) => { | |||
this.navlinks.forEach((link: DropdownLinkDirective) => { | |||
if (link.group) { | |||
const routeUrl = this.getUrl(); | |||
const currentUrl = routeUrl.split('/'); | |||
if (currentUrl.indexOf( link.group ) > 0) { | |||
link.open = true; | |||
this.closeOtherLinks(link); | |||
} | |||
} | |||
}); | |||
}); | |||
} | |||
constructor( private router: Router) {} | |||
} |
@ -0,0 +1,9 @@ | |||
import { Directive, ElementRef, Attribute, OnInit } from '@angular/core'; | |||
@Directive({ selector: '[fontSize]' }) | |||
export class FontSizeDirective implements OnInit { | |||
constructor( @Attribute('fontSize') public fontSize: string, private el: ElementRef) { } | |||
ngOnInit() { | |||
this.el.nativeElement.fontSize = this.fontSize; | |||
} | |||
} |
@ -0,0 +1,82 @@ | |||
import { | |||
Directive, | |||
ElementRef, | |||
Attribute, | |||
OnInit, | |||
Input, | |||
Renderer2, | |||
NgZone, | |||
SimpleChanges, | |||
OnChanges, | |||
OnDestroy, | |||
ChangeDetectorRef | |||
} from "@angular/core"; | |||
import * as hl from "highlight.js"; | |||
import { HttpClient } from "@angular/common/http"; | |||
import { Subject } from "rxjs"; | |||
import { takeUntil } from "rxjs/operators"; | |||
@Directive({ | |||
host: { | |||
"[class.hljs]": "true", | |||
"[innerHTML]": "highlightedCode" | |||
}, | |||
selector: "[matxHighlight]" | |||
}) | |||
export class MatXHighlightDirective implements OnInit, OnChanges, OnDestroy { | |||
constructor( | |||
private el: ElementRef, | |||
private cdr: ChangeDetectorRef, | |||
private _zone: NgZone, | |||
private http: HttpClient | |||
) { | |||
this.unsubscribeAll = new Subject(); | |||
} | |||
// Inner highlighted html | |||
highlightedCode: string; | |||
@Input() path: string; | |||
@Input("matxHighlight") code: string; | |||
private unsubscribeAll: Subject<any>; | |||
@Input() languages: string[]; | |||
ngOnInit() { | |||
if (this.code) { | |||
this.highlightElement(this.code); | |||
} | |||
if (this.path) { | |||
this.highlightedCode = "Loading..." | |||
this.http | |||
.get(this.path, { responseType: "text" }) | |||
.pipe(takeUntil(this.unsubscribeAll)) | |||
.subscribe(response => { | |||
this.highlightElement(response, this.languages); | |||
}); | |||
} | |||
} | |||
ngOnDestroy() { | |||
this.unsubscribeAll.next(); | |||
this.unsubscribeAll.complete(); | |||
} | |||
ngOnChanges(changes: SimpleChanges) { | |||
if ( | |||
changes["code"] && | |||
changes["code"].currentValue && | |||
changes["code"].currentValue !== changes["code"].previousValue | |||
) { | |||
this.highlightElement(this.code); | |||
// console.log('hljs on change', changes) | |||
} | |||
} | |||
highlightElement(code: string, languages?: string[]) { | |||
this._zone.runOutsideAngular(() => { | |||
const res = hl.highlightAuto(code); | |||
this.highlightedCode = res.value; | |||
// this.cdr.detectChanges(); | |||
// console.log(languages) | |||
}); | |||
} | |||
} |
@ -0,0 +1,46 @@ | |||
import { Directive, Host, Self, Optional, OnDestroy, OnInit } from '@angular/core'; | |||
import { MediaChange, MediaObserver } from "@angular/flex-layout"; | |||
import { Subscription } from "rxjs"; | |||
import { MatSidenav } from '@angular/material/sidenav'; | |||
@Directive({ | |||
selector: '[MatXSideNavToggle]' | |||
}) | |||
export class MatXSideNavToggleDirective implements OnInit, OnDestroy { | |||
isMobile; | |||
screenSizeWatcher: Subscription; | |||
constructor( | |||
private mediaObserver: MediaObserver, | |||
@Host() @Self() @Optional() public sideNav: MatSidenav | |||
) { | |||
} | |||
ngOnInit() { | |||
this.initSideNav(); | |||
} | |||
ngOnDestroy() { | |||
if(this.screenSizeWatcher) { | |||
this.screenSizeWatcher.unsubscribe() | |||
} | |||
} | |||
updateSidenav() { | |||
var self = this; | |||
setTimeout(() => { | |||
self.sideNav.opened = !self.isMobile; | |||
self.sideNav.mode = self.isMobile ? 'over' : 'side'; | |||
}) | |||
} | |||
initSideNav() { | |||
this.isMobile = this.mediaObserver.isActive('xs') || this.mediaObserver.isActive('sm'); | |||
// console.log(this.isMobile) | |||
this.updateSidenav(); | |||
this.screenSizeWatcher = this.mediaObserver.media$.subscribe((change: MediaChange) => { | |||
this.isMobile = (change.mqAlias == 'xs') || (change.mqAlias == 'sm'); | |||
this.updateSidenav(); | |||
}); | |||
} | |||
} |
@ -0,0 +1,91 @@ | |||
import { | |||
Directive, | |||
OnInit, | |||
OnDestroy, | |||
HostBinding, | |||
Input, | |||
HostListener | |||
} from "@angular/core"; | |||
import { takeUntil } from "rxjs/operators"; | |||
import { Subject } from "rxjs"; | |||
import { MatchMediaService } from "app/shared/services/match-media.service"; | |||
import { MatXSidenavHelperService } from "./matx-sidenav-helper.service"; | |||
import { MatSidenav } from "@angular/material/sidenav"; | |||
import { MediaObserver } from "@angular/flex-layout"; | |||
@Directive({ | |||
selector: "[matxSidenavHelper]" | |||
}) | |||
export class MatXSidenavHelperDirective implements OnInit, OnDestroy { | |||
@HostBinding("class.is-open") | |||
isOpen: boolean; | |||
@Input("matxSidenavHelper") | |||
id: string; | |||
@Input("isOpen") | |||
isOpenBreakpoint: string; | |||
private unsubscribeAll: Subject<any>; | |||
constructor( | |||
private matchMediaService: MatchMediaService, | |||
private matxSidenavHelperService: MatXSidenavHelperService, | |||
private matSidenav: MatSidenav, | |||
private mediaObserver: MediaObserver | |||
) { | |||
// Set the default value | |||
this.isOpen = true; | |||
this.unsubscribeAll = new Subject(); | |||
} | |||
ngOnInit(): void { | |||
this.matxSidenavHelperService.setSidenav(this.id, this.matSidenav); | |||
if (this.mediaObserver.isActive(this.isOpenBreakpoint)) { | |||
this.isOpen = true; | |||
this.matSidenav.mode = "side"; | |||
this.matSidenav.toggle(true); | |||
} else { | |||
this.isOpen = false; | |||
this.matSidenav.mode = "over"; | |||
this.matSidenav.toggle(false); | |||
} | |||
this.matchMediaService.onMediaChange | |||
.pipe(takeUntil(this.unsubscribeAll)) | |||
.subscribe(() => { | |||
if (this.mediaObserver.isActive(this.isOpenBreakpoint)) { | |||
this.isOpen = true; | |||
this.matSidenav.mode = "side"; | |||
this.matSidenav.toggle(true); | |||
} else { | |||
this.isOpen = false; | |||
this.matSidenav.mode = "over"; | |||
this.matSidenav.toggle(false); | |||
} | |||
}); | |||
} | |||
ngOnDestroy(): void { | |||
this.unsubscribeAll.next(); | |||
this.unsubscribeAll.complete(); | |||
} | |||
} | |||
@Directive({ | |||
selector: "[matxSidenavToggler]" | |||
}) | |||
export class MatXSidenavTogglerDirective { | |||
@Input("matxSidenavToggler") | |||
public id: any; | |||
constructor(private matxSidenavHelperService: MatXSidenavHelperService) {} | |||
@HostListener("click") | |||
onClick() { | |||
// console.log(this.matxSidenavHelperService.getSidenav(this.id)) | |||
this.matxSidenavHelperService.getSidenav(this.id).toggle(); | |||
} | |||
} |
@ -0,0 +1,21 @@ | |||
import { Injectable } from "@angular/core"; | |||
import { MatSidenav } from "@angular/material/sidenav"; | |||
@Injectable({ | |||
providedIn: "root" | |||
}) | |||
export class MatXSidenavHelperService { | |||
sidenavList: MatSidenav[]; | |||
constructor() { | |||
this.sidenavList = []; | |||
} | |||
setSidenav(id, sidenav): void { | |||
this.sidenavList[id] = sidenav; | |||
} | |||
getSidenav(id): any { | |||
return this.sidenavList[id]; | |||
} | |||
} |
@ -0,0 +1,64 @@ | |||
import { Directive, ElementRef, Attribute, OnInit, HostListener } from '@angular/core'; | |||
@Directive({ selector: '[scrollTo]' }) | |||
export class ScrollToDirective implements OnInit { | |||
constructor( @Attribute('scrollTo') public elmID: string, private el: ElementRef) { } | |||
ngOnInit() {} | |||
currentYPosition() { | |||
// Firefox, Chrome, Opera, Safari | |||
if (self.pageYOffset) return self.pageYOffset; | |||
// Internet Explorer 6 - standards mode | |||
if (document.documentElement && document.documentElement.scrollTop) | |||
return document.documentElement.scrollTop; | |||
// Internet Explorer 6, 7 and 8 | |||
if (document.body.scrollTop) return document.body.scrollTop; | |||
return 0; | |||
}; | |||
elmYPosition(eID) { | |||
var elm = document.getElementById(eID); | |||
var y = elm.offsetTop; | |||
var node: any = elm; | |||
while (node.offsetParent && node.offsetParent != document.body) { | |||
node = node.offsetParent; | |||
y += node.offsetTop; | |||
} | |||
return y; | |||
}; | |||
@HostListener('click', ['$event']) | |||
smoothScroll() { | |||
if(!this.elmID) | |||
return; | |||
var startY = this.currentYPosition(); | |||
var stopY = this.elmYPosition(this.elmID); | |||
var distance = stopY > startY ? stopY - startY : startY - stopY; | |||
if (distance < 100) { | |||
scrollTo(0, stopY); | |||
return; | |||
} | |||
var speed = Math.round(distance / 50); | |||
if (speed >= 20) speed = 20; | |||
var step = Math.round(distance / 25); | |||
var leapY = stopY > startY ? startY + step : startY - step; | |||
var timer = 0; | |||
if (stopY > startY) { | |||
for (var i = startY; i < stopY; i += step) { | |||
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed); | |||
leapY += step; | |||
if (leapY > stopY) leapY = stopY; | |||
timer++; | |||
} | |||
return; | |||
} | |||
for (var i = startY; i > stopY; i -= step) { | |||
setTimeout("window.scrollTo(0, " + leapY + ")", timer * speed); | |||
leapY -= step; | |||
if (leapY < stopY) leapY = stopY; | |||
timer++; | |||
} | |||
return false; | |||
}; | |||
} |
@ -0,0 +1,33 @@ | |||
import { NgModule } from '@angular/core'; | |||
import { CommonModule } from '@angular/common'; | |||
import { FontSizeDirective } from './font-size.directive'; | |||
import { ScrollToDirective } from './scroll-to.directive'; | |||
import { AppDropdownDirective } from './dropdown.directive'; | |||
import { DropdownAnchorDirective } from './dropdown-anchor.directive'; | |||
import { DropdownLinkDirective } from './dropdown-link.directive'; | |||
import { MatXSideNavToggleDirective } from './matx-side-nav-toggle.directive'; | |||
import { MatXSidenavHelperDirective, MatXSidenavTogglerDirective } from './matx-sidenav-helper/matx-sidenav-helper.directive'; | |||
import { MatXHighlightDirective } from './matx-highlight.directive'; | |||
const directives = [ | |||
FontSizeDirective, | |||
ScrollToDirective, | |||
AppDropdownDirective, | |||
DropdownAnchorDirective, | |||
DropdownLinkDirective, | |||
MatXSideNavToggleDirective, | |||
MatXSidenavHelperDirective, | |||
MatXSidenavTogglerDirective, | |||
MatXHighlightDirective | |||
] | |||
@NgModule({ | |||
imports: [ | |||
CommonModule | |||
], | |||
declarations: directives, | |||
exports: directives | |||
}) | |||
export class SharedDirectivesModule {} |
@ -0,0 +1,27 @@ | |||
import { Injectable } from '@angular/core'; | |||
import { | |||
CanActivate, | |||
ActivatedRouteSnapshot, | |||
RouterStateSnapshot, | |||
Router, | |||
} from '@angular/router'; | |||
import { JwtAuthService } from '../services/auth/jwt-auth.service'; | |||
@Injectable() | |||
export class AuthGuard implements CanActivate { | |||
constructor(private router: Router, private jwtAuth: JwtAuthService) {} | |||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | |||
if (this.jwtAuth.isLoggedIn()) { | |||
return true; | |||
} else { | |||
this.router.navigate(['/sessions/signin'], { | |||
queryParams: { | |||
return: state.url | |||
} | |||
}); | |||
return false; | |||
} | |||
} | |||
} |
@ -0,0 +1,31 @@ | |||
import { Injectable } from "@angular/core"; | |||
import { | |||
CanActivate, | |||
ActivatedRouteSnapshot, | |||
RouterStateSnapshot | |||
} from "@angular/router"; | |||
import { JwtAuthService } from "../services/auth/jwt-auth.service"; | |||
import { MatSnackBar } from "@angular/material/snack-bar"; | |||
@Injectable() | |||
export class UserRoleGuard implements CanActivate { | |||
constructor( | |||
private jwtAuth: JwtAuthService, | |||
private snack: MatSnackBar | |||
) {} | |||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | |||
var user = this.jwtAuth.getUser(); | |||
if (route?.data?.roles?.includes(user.role)) { | |||
return true; | |||
} else { | |||
this.snack.open("You do not have access to this page!", "View Documentaion") | |||
.onAction() | |||
.subscribe(() => { | |||
window.open('http://demos.ui-lib.com/matx-angular-doc/authentication.html', '_blank'); | |||
}); | |||
return false; | |||
} | |||
} | |||
} |
@ -0,0 +1,10 @@ | |||
export function getQueryParam(prop) { | |||
var params = {}; | |||
var search = decodeURIComponent(window.location.href.slice(window.location.href.indexOf('?') + 1)); | |||
var definitions = search.split('&'); | |||
definitions.forEach(function (val, key) { | |||
var parts = val.split('=', 2); | |||
params[parts[0]] = parts[1]; | |||
}); | |||
return (prop && prop in params) ? params[prop] : params; | |||
} |
@ -0,0 +1,81 @@ | |||
export function getIndexBy(array: Array<{}>, { name, value }): number { | |||
for (let i = 0; i < array.length; i++) { | |||
if (array[i][name] === value) { | |||
return i; | |||
} | |||
} | |||
return -1; | |||
} | |||
function currentYPosition() { | |||
if (!window) { | |||
return; | |||
} | |||
// Firefox, Chrome, Opera, Safari | |||
if (window.pageYOffset) return window.pageYOffset; | |||
// Internet Explorer 6 - standards mode | |||
if (document.documentElement && document.documentElement.scrollTop) | |||
return document.documentElement.scrollTop; | |||
// Internet Explorer 6, 7 and 8 | |||
if (document.body.scrollTop) return document.body.scrollTop; | |||
return 0; | |||
} | |||
function elmYPosition(elm) { | |||
var y = elm.offsetTop; | |||
var node = elm; | |||
while (node.offsetParent && node.offsetParent !== document.body) { | |||
node = node.offsetParent; | |||
y += node.offsetTop; | |||
} | |||
return y; | |||
} | |||
export function scrollTo(selector) { | |||
var elm = document.querySelector(selector); | |||
if (!selector || !elm) { | |||
return; | |||
} | |||
var startY = currentYPosition(); | |||
var stopY = elmYPosition(elm); | |||
var distance = stopY > startY ? stopY - startY : startY - stopY; | |||
if (distance < 100) { | |||
window.scrollTo(0, stopY); | |||
return; | |||
} | |||
var speed = Math.round(distance / 50); | |||
if (speed >= 20) speed = 20; | |||
var step = Math.round(distance / 25); | |||
var leapY = stopY > startY ? startY + step : startY - step; | |||
var timer = 0; | |||
if (stopY > startY) { | |||
for (var i = startY; i < stopY; i += step) { | |||
setTimeout( | |||
(function(leapY) { | |||
return () => { | |||
window.scrollTo(0, leapY); | |||
}; | |||
})(leapY), | |||
timer * speed | |||
); | |||
leapY += step; | |||
if (leapY > stopY) leapY = stopY; | |||
timer++; | |||
} | |||
return; | |||
} | |||
for (let i = startY; i > stopY; i -= step) { | |||
setTimeout( | |||
(function(leapY) { | |||
return () => { | |||
window.scrollTo(0, leapY); | |||
}; | |||
})(leapY), | |||
timer * speed | |||
); | |||
leapY -= step; | |||
if (leapY < stopY) leapY = stopY; | |||
timer++; | |||
} | |||
return false; | |||
} |