initial commit
This commit is contained in:
commit
8e72ca4939
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -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.
|
67
README.md
Normal file
67
README.md
Normal file
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
143
angular.json
Normal file
143
angular.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
11
e2e/app.po.ts
Normal file
11
e2e/app.po.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
14
e2e/tsconfig.e2e.json
Normal file
14
e2e/tsconfig.e2e.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/e2e",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
33
karma.conf.js
Normal file
33
karma.conf.js
Normal file
@ -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
|
||||||
|
});
|
||||||
|
};
|
15329
package-lock.json
generated
Normal file
15329
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
package.json
Normal file
74
package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
28
protractor.conf.js
Normal file
28
protractor.conf.js
Normal file
@ -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 } }));
|
||||||
|
}
|
||||||
|
};
|
6
proxy.json
Normal file
6
proxy.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"/api": {
|
||||||
|
"target": "http://localhost:9000",
|
||||||
|
"secure": false
|
||||||
|
}
|
||||||
|
}
|
26
server/auth.route.ts
Normal file
26
server/auth.route.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
29
server/create-course.route.ts
Normal file
29
server/create-course.route.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
630
server/db-data.ts
Normal file
630
server/db-data.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
server/delete-course.route.ts
Normal file
22
server/delete-course.route.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
38
server/get-courses.route.ts
Normal file
38
server/get-courses.route.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
24
server/save-course.route.ts
Normal file
24
server/save-course.route.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
40
server/search-lessons.route.ts
Normal file
40
server/search-lessons.route.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
45
server/server.ts
Normal file
45
server/server.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
6
server/server.tsconfig.json
Normal file
6
server/server.tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["es2017"]
|
||||||
|
}
|
||||||
|
}
|
17
src/app/app.component.css
Normal file
17
src/app/app.component.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
1
src/app/app.component.html
Normal file
1
src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<router-outlet></router-outlet>
|
73
src/app/app.component.ts
Normal file
73
src/app/app.component.ts
Normal file
@ -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());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
71
src/app/app.module.ts
Normal file
71
src/app/app.module.ts
Normal file
@ -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 {
|
||||||
|
}
|
80
src/app/app.routing.ts
Normal file
80
src/app/app.routing.ts
Normal file
@ -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',
|
||||||
|
},
|
||||||
|
];
|
5
src/app/auth/action-types.ts
Normal file
5
src/app/auth/action-types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import * as AuthActions from './auth.actions';
|
||||||
|
|
||||||
|
export {AuthActions};
|
14
src/app/auth/auth.actions.ts
Normal file
14
src/app/auth/auth.actions.ts
Normal file
@ -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"
|
||||||
|
);
|
39
src/app/auth/auth.effects.ts
Normal file
39
src/app/auth/auth.effects.ts
Normal file
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/app/auth/auth.guard.ts
Normal file
37
src/app/auth/auth.guard.ts
Normal file
@ -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');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
51
src/app/auth/auth.module.ts
Normal file
51
src/app/auth/auth.module.ts
Normal file
@ -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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
src/app/auth/auth.selectors.ts
Normal file
19
src/app/auth/auth.selectors.ts
Normal file
@ -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
|
||||||
|
);
|
20
src/app/auth/auth.service.ts
Normal file
20
src/app/auth/auth.service.ts
Normal file
@ -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});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
92
src/app/auth/login/login.component.html
Normal file
92
src/app/auth/login/login.component.html
Normal file
@ -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>
|
11
src/app/auth/login/login.component.scss
Normal file
11
src/app/auth/login/login.component.scss
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
max-width: 350px;
|
||||||
|
margin: 50px auto 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
152
src/app/auth/login/login.component.ts
Normal file
152
src/app/auth/login/login.component.ts
Normal file
@ -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')
|
||||||
|
// );
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
6
src/app/auth/model/user.model.ts
Normal file
6
src/app/auth/model/user.model.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
40
src/app/auth/reducers/index.ts
Normal file
40
src/app/auth/reducers/index.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
|
56
src/app/courses/course/course.component.css
Normal file
56
src/app/courses/course/course.component.css
Normal file
@ -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;
|
||||||
|
}
|
51
src/app/courses/course/course.component.html
Normal file
51
src/app/courses/course/course.component.html
Normal file
@ -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>
|
73
src/app/courses/course/course.component.ts
Normal file
73
src/app/courses/course/course.component.ts
Normal file
@ -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)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
120
src/app/courses/courses.module.ts
Normal file
120
src/app/courses/courses.module.ts
Normal file
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
38
src/app/courses/home/home.component.css
Normal file
38
src/app/courses/home/home.component.css
Normal file
@ -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;
|
||||||
|
}
|
41
src/app/courses/home/home.component.html
Normal file
41
src/app/courses/home/home.component.html
Normal file
@ -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>
|
||||||
|
|
68
src/app/courses/home/home.component.ts
Normal file
68
src/app/courses/home/home.component.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
28
src/app/courses/model/course.ts
Normal file
28
src/app/courses/model/course.ts
Normal file
@ -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;
|
||||||
|
|
||||||
|
}
|
26
src/app/courses/model/lesson.ts
Normal file
26
src/app/courses/model/lesson.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
19
src/app/courses/services/course-entity.service.ts
Normal file
19
src/app/courses/services/course-entity.service.ts
Normal file
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
26
src/app/courses/services/courses-data.service.ts
Normal file
26
src/app/courses/services/courses-data.service.ts
Normal file
@ -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"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
48
src/app/courses/services/courses-http.service.ts
Normal file
48
src/app/courses/services/courses-http.service.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
31
src/app/courses/services/courses.resolver.ts
Normal file
31
src/app/courses/services/courses.resolver.ts
Normal file
@ -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()
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
src/app/courses/services/lesson-entity.service.ts
Normal file
13
src/app/courses/services/lesson-entity.service.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/app/courses/shared/default-dialog-config.ts
Normal file
12
src/app/courses/shared/default-dialog-config.ts
Normal file
@ -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;
|
||||||
|
}
|
20
src/app/reducers/index.ts
Normal file
20
src/app/reducers/index.ts
Normal file
@ -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] : [];
|
||||||
|
|
||||||
|
|
53
src/app/shared/animations/matx-animations.ts
Normal file
53
src/app/shared/animations/matx-animations.ts
Normal file
@ -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>
|
66
src/app/shared/components/breadcrumb/breadcrumb.component.ts
Normal file
66
src/app/shared/components/breadcrumb/breadcrumb.component.ts
Normal file
@ -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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
131
src/app/shared/components/customizer/customizer.component.html
Normal file
131
src/app/shared/components/customizer/customizer.component.html
Normal file
@ -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>
|
118
src/app/shared/components/customizer/customizer.component.scss
Normal file
118
src/app/shared/components/customizer/customizer.component.scss
Normal file
@ -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;
|
||||||
|
// }
|
||||||
|
// }
|
80
src/app/shared/components/customizer/customizer.component.ts
Normal file
80
src/app/shared/components/customizer/customizer.component.ts
Normal file
@ -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})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
6
src/app/shared/components/footer/footer.component.html
Normal file
6
src/app/shared/components/footer/footer.component.html
Normal file
@ -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>
|
25
src/app/shared/components/footer/footer.component.spec.ts
Normal file
25
src/app/shared/components/footer/footer.component.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
15
src/app/shared/components/footer/footer.component.ts
Normal file
15
src/app/shared/components/footer/footer.component.ts
Normal file
@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
66
src/app/shared/components/shared-components.module.ts
Normal file
66
src/app/shared/components/shared-components.module.ts
Normal file
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
29
src/app/shared/components/sidenav/sidenav.component.ts
Normal file
29
src/app/shared/components/sidenav/sidenav.component.ts
Normal file
@ -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" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
133
src/app/shared/components/sidenav/sidenav.template.html
Normal file
133
src/app/shared/components/sidenav/sidenav.template.html
Normal file
@ -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>
|
19
src/app/shared/directives/dropdown-anchor.directive.ts
Normal file
19
src/app/shared/directives/dropdown-anchor.directive.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
46
src/app/shared/directives/dropdown-link.directive.ts
Normal file
46
src/app/shared/directives/dropdown-link.directive.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
55
src/app/shared/directives/dropdown.directive.ts
Normal file
55
src/app/shared/directives/dropdown.directive.ts
Normal file
@ -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) {}
|
||||||
|
|
||||||
|
}
|
9
src/app/shared/directives/font-size.directive.ts
Normal file
9
src/app/shared/directives/font-size.directive.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
82
src/app/shared/directives/matx-highlight.directive.ts
Normal file
82
src/app/shared/directives/matx-highlight.directive.ts
Normal file
@ -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)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
46
src/app/shared/directives/matx-side-nav-toggle.directive.ts
Normal file
46
src/app/shared/directives/matx-side-nav-toggle.directive.ts
Normal file
@ -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];
|
||||||
|
}
|
||||||
|
}
|
64
src/app/shared/directives/scroll-to.directive.ts
Normal file
64
src/app/shared/directives/scroll-to.directive.ts
Normal file
@ -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;
|
||||||
|
};
|
||||||
|
}
|
33
src/app/shared/directives/shared-directives.module.ts
Normal file
33
src/app/shared/directives/shared-directives.module.ts
Normal file
@ -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 {}
|
27
src/app/shared/guards/auth.guard.ts
Normal file
27
src/app/shared/guards/auth.guard.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/app/shared/guards/user-role.guard.ts
Normal file
31
src/app/shared/guards/user-role.guard.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
src/app/shared/helpers/url.helper.ts
Normal file
10
src/app/shared/helpers/url.helper.ts
Normal file
@ -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;
|
||||||
|
}
|
81
src/app/shared/helpers/utils.ts
Normal file
81
src/app/shared/helpers/utils.ts
Normal file
@ -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;
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user