Browse Source

initial commit

master
commit
8e72ca4939
1210 changed files with 51412 additions and 0 deletions
  1. +21
    -0
      LICENSE
  2. +67
    -0
      README.md
  3. +143
    -0
      angular.json
  4. +11
    -0
      e2e/app.po.ts
  5. +14
    -0
      e2e/tsconfig.e2e.json
  6. +33
    -0
      karma.conf.js
  7. +15329
    -0
      package-lock.json
  8. +74
    -0
      package.json
  9. +28
    -0
      protractor.conf.js
  10. +6
    -0
      proxy.json
  11. +26
    -0
      server/auth.route.ts
  12. +29
    -0
      server/create-course.route.ts
  13. +630
    -0
      server/db-data.ts
  14. +22
    -0
      server/delete-course.route.ts
  15. +38
    -0
      server/get-courses.route.ts
  16. +24
    -0
      server/save-course.route.ts
  17. +40
    -0
      server/search-lessons.route.ts
  18. +45
    -0
      server/server.ts
  19. +6
    -0
      server/server.tsconfig.json
  20. +17
    -0
      src/app/app.component.css
  21. +1
    -0
      src/app/app.component.html
  22. +73
    -0
      src/app/app.component.ts
  23. +71
    -0
      src/app/app.module.ts
  24. +80
    -0
      src/app/app.routing.ts
  25. +5
    -0
      src/app/auth/action-types.ts
  26. +14
    -0
      src/app/auth/auth.actions.ts
  27. +39
    -0
      src/app/auth/auth.effects.ts
  28. +37
    -0
      src/app/auth/auth.guard.ts
  29. +51
    -0
      src/app/auth/auth.module.ts
  30. +19
    -0
      src/app/auth/auth.selectors.ts
  31. +20
    -0
      src/app/auth/auth.service.ts
  32. +92
    -0
      src/app/auth/login/login.component.html
  33. +11
    -0
      src/app/auth/login/login.component.scss
  34. +152
    -0
      src/app/auth/login/login.component.ts
  35. +6
    -0
      src/app/auth/model/user.model.ts
  36. +40
    -0
      src/app/auth/reducers/index.ts
  37. +56
    -0
      src/app/courses/course/course.component.css
  38. +51
    -0
      src/app/courses/course/course.component.html
  39. +73
    -0
      src/app/courses/course/course.component.ts
  40. +15
    -0
      src/app/courses/courses-card-list/courses-card-list.component.css
  41. +33
    -0
      src/app/courses/courses-card-list/courses-card-list.component.html
  42. +67
    -0
      src/app/courses/courses-card-list/courses-card-list.component.ts
  43. +120
    -0
      src/app/courses/courses.module.ts
  44. +10
    -0
      src/app/courses/edit-course-dialog/edit-course-dialog.component.css
  45. +95
    -0
      src/app/courses/edit-course-dialog/edit-course-dialog.component.html
  46. +91
    -0
      src/app/courses/edit-course-dialog/edit-course-dialog.component.ts
  47. +38
    -0
      src/app/courses/home/home.component.css
  48. +41
    -0
      src/app/courses/home/home.component.html
  49. +68
    -0
      src/app/courses/home/home.component.ts
  50. +28
    -0
      src/app/courses/model/course.ts
  51. +26
    -0
      src/app/courses/model/lesson.ts
  52. +19
    -0
      src/app/courses/services/course-entity.service.ts
  53. +26
    -0
      src/app/courses/services/courses-data.service.ts
  54. +48
    -0
      src/app/courses/services/courses-http.service.ts
  55. +31
    -0
      src/app/courses/services/courses.resolver.ts
  56. +13
    -0
      src/app/courses/services/lesson-entity.service.ts
  57. +12
    -0
      src/app/courses/shared/default-dialog-config.ts
  58. +20
    -0
      src/app/reducers/index.ts
  59. +53
    -0
      src/app/shared/animations/matx-animations.ts
  60. +12
    -0
      src/app/shared/components/breadcrumb/breadcrumb.component.html
  61. +0
    -0
      src/app/shared/components/breadcrumb/breadcrumb.component.scss
  62. +66
    -0
      src/app/shared/components/breadcrumb/breadcrumb.component.ts
  63. +12
    -0
      src/app/shared/components/button-loading/button-loading.component.html
  64. +0
    -0
      src/app/shared/components/button-loading/button-loading.component.scss
  65. +25
    -0
      src/app/shared/components/button-loading/button-loading.component.spec.ts
  66. +23
    -0
      src/app/shared/components/button-loading/button-loading.component.ts
  67. +131
    -0
      src/app/shared/components/customizer/customizer.component.html
  68. +118
    -0
      src/app/shared/components/customizer/customizer.component.scss
  69. +80
    -0
      src/app/shared/components/customizer/customizer.component.ts
  70. +6
    -0
      src/app/shared/components/footer/footer.component.html
  71. +0
    -0
      src/app/shared/components/footer/footer.component.scss
  72. +25
    -0
      src/app/shared/components/footer/footer.component.spec.ts
  73. +15
    -0
      src/app/shared/components/footer/footer.component.ts
  74. +75
    -0
      src/app/shared/components/header-side/header-side.component.ts
  75. +77
    -0
      src/app/shared/components/header-side/header-side.template.html
  76. +138
    -0
      src/app/shared/components/layouts/admin-layout/admin-layout.component.ts
  77. +66
    -0
      src/app/shared/components/layouts/admin-layout/admin-layout.template.html
  78. +1
    -0
      src/app/shared/components/layouts/auth-layout/auth-layout.component.html
  79. +14
    -0
      src/app/shared/components/layouts/auth-layout/auth-layout.component.ts
  80. +18
    -0
      src/app/shared/components/notifications/notifications.component.html
  81. +46
    -0
      src/app/shared/components/notifications/notifications.component.ts
  82. +66
    -0
      src/app/shared/components/shared-components.module.ts
  83. +95
    -0
      src/app/shared/components/sidebar-side/sidebar-side.component.html
  84. +48
    -0
      src/app/shared/components/sidebar-side/sidebar-side.component.ts
  85. +29
    -0
      src/app/shared/components/sidenav/sidenav.component.ts
  86. +133
    -0
      src/app/shared/components/sidenav/sidenav.template.html
  87. +19
    -0
      src/app/shared/directives/dropdown-anchor.directive.ts
  88. +46
    -0
      src/app/shared/directives/dropdown-link.directive.ts
  89. +55
    -0
      src/app/shared/directives/dropdown.directive.ts
  90. +9
    -0
      src/app/shared/directives/font-size.directive.ts
  91. +82
    -0
      src/app/shared/directives/matx-highlight.directive.ts
  92. +46
    -0
      src/app/shared/directives/matx-side-nav-toggle.directive.ts
  93. +91
    -0
      src/app/shared/directives/matx-sidenav-helper/matx-sidenav-helper.directive.ts
  94. +21
    -0
      src/app/shared/directives/matx-sidenav-helper/matx-sidenav-helper.service.ts
  95. +64
    -0
      src/app/shared/directives/scroll-to.directive.ts
  96. +33
    -0
      src/app/shared/directives/shared-directives.module.ts
  97. +27
    -0
      src/app/shared/guards/auth.guard.ts
  98. +31
    -0
      src/app/shared/guards/user-role.guard.ts
  99. +10
    -0
      src/app/shared/helpers/url.helper.ts
  100. +81
    -0
      src/app/shared/helpers/utils.ts

+ 21
- 0
LICENSE View 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
- 0
README.md View 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
- 0
angular.json View 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
- 0
e2e/app.po.ts View 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
- 0
e2e/tsconfig.e2e.json View File

@ -0,0 +1,14 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

+ 33
- 0
karma.conf.js View 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
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 74
- 0
package.json View 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
- 0
protractor.conf.js View 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
- 0
proxy.json View File

@ -0,0 +1,6 @@
{
"/api": {
"target": "http://localhost:9000",
"secure": false
}
}

+ 26
- 0
server/auth.route.ts View 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
- 0
server/create-course.route.ts View 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
- 0
server/db-data.ts View 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
- 0
server/delete-course.route.ts View 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
- 0
server/get-courses.route.ts View 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
- 0
server/save-course.route.ts View 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
- 0
server/search-lessons.route.ts View 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
- 0
server/server.ts View 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
- 0
server/server.tsconfig.json View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"module": "commonjs",
"lib": ["es2017"]
}
}

+ 17
- 0
src/app/app.component.css View 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
- 0
src/app/app.component.html View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

+ 73
- 0
src/app/app.component.ts View 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
- 0
src/app/app.module.ts View 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
- 0
src/app/app.routing.ts View 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
- 0
src/app/auth/action-types.ts View File

@ -0,0 +1,5 @@
import * as AuthActions from './auth.actions';
export {AuthActions};

+ 14
- 0
src/app/auth/auth.actions.ts View 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
- 0
src/app/auth/auth.effects.ts View 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
- 0
src/app/auth/auth.guard.ts View 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
- 0
src/app/auth/auth.module.ts View 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
- 0
src/app/auth/auth.selectors.ts View 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
- 0
src/app/auth/auth.service.ts View 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
- 0
src/app/auth/login/login.component.html View 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
- 0
src/app/auth/login/login.component.scss View File

@ -0,0 +1,11 @@
.login-page {
max-width: 350px;
margin: 50px auto 0 auto;
}
.login-form {
display: flex;
flex-direction: column;
}

+ 152
- 0
src/app/auth/login/login.component.ts View 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
- 0
src/app/auth/model/user.model.ts View File

@ -0,0 +1,6 @@
export interface User {
id: string;
email: string;
}

+ 40
- 0
src/app/auth/reducers/index.ts View 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
- 0
src/app/courses/course/course.component.css View 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
- 0
src/app/courses/course/course.component.html View 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
- 0
src/app/courses/course/course.component.ts View 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;
}
}

+ 15
- 0
src/app/courses/courses-card-list/courses-card-list.component.css View File

@ -0,0 +1,15 @@
.course-card {
margin: 20px 10px;
}
.course-actions {
text-align: center;
}
.course-actions button {
margin-right: 10px;
}

+ 33
- 0
src/app/courses/courses-card-list/courses-card-list.component.html View File

@ -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>

+ 67
- 0
src/app/courses/courses-card-list/courses-card-list.component.ts View File

@ -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
- 0
src/app/courses/courses.module.ts View 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);
}
}

+ 10
- 0
src/app/courses/edit-course-dialog/edit-course-dialog.component.css View File

@ -0,0 +1,10 @@
.mat-form-field {
display: block;
}
textarea {
height: 100px;
resize: vertical;
}

+ 95
- 0
src/app/courses/edit-course-dialog/edit-course-dialog.component.html View File

@ -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>

+ 91
- 0
src/app/courses/edit-course-dialog/edit-course-dialog.component.ts View File

@ -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
- 0
src/app/courses/home/home.component.css View 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
- 0
src/app/courses/home/home.component.html View 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
- 0
src/app/courses/home/home.component.ts View 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
- 0
src/app/courses/model/course.ts View 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
- 0
src/app/courses/model/lesson.ts View 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
- 0
src/app/courses/services/course-entity.service.ts View 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
- 0
src/app/courses/services/courses-data.service.ts View 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
- 0
src/app/courses/services/courses-http.service.ts View 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
- 0
src/app/courses/services/courses.resolver.ts View 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
- 0
src/app/courses/services/lesson-entity.service.ts View 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
- 0
src/app/courses/shared/default-dialog-config.ts View 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
- 0
src/app/reducers/index.ts View 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
- 0
src/app/shared/animations/matx-animations.ts View 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"))
])
];

+ 12
- 0
src/app/shared/components/breadcrumb/breadcrumb.component.html View File

@ -0,0 +1,12 @@
<div class="breadcrumb-bar" *ngIf="layout.layoutConf.useBreadcrumb && layout.layoutConf.breadcrumb === 'simple'">
<ul class="breadcrumb">
<li *ngFor="let part of routeParts"><a routerLink="/{{part.url}}">{{part.breadcrumb}}</a></li>
</ul>
</div>
<div class="breadcrumb-title" *ngIf="layout.layoutConf.useBreadcrumb && layout.layoutConf.breadcrumb === 'title'">
<h1 class="bc-title">{{routeParts[routeParts.length -1]?.breadcrumb}}</h1>
<ul class="breadcrumb" *ngIf="routeParts.length > 1">
<li *ngFor="let part of routeParts"><a routerLink="/{{part.url}}" class="text-muted">{{part.breadcrumb}}</a></li>
</ul>
</div>

+ 0
- 0
src/app/shared/components/breadcrumb/breadcrumb.component.scss View File


+ 66
- 0
src/app/shared/components/breadcrumb/breadcrumb.component.ts View 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;
}
}

+ 12
- 0
src/app/shared/components/button-loading/button-loading.component.html View File

@ -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
src/app/shared/components/button-loading/button-loading.component.scss View File


+ 25
- 0
src/app/shared/components/button-loading/button-loading.component.spec.ts View File

@ -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();
});
});

+ 23
- 0
src/app/shared/components/button-loading/button-loading.component.ts View File

@ -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
- 0
src/app/shared/components/customizer/customizer.component.html View 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
- 0
src/app/shared/components/customizer/customizer.component.scss View 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
- 0
src/app/shared/components/customizer/customizer.component.ts View 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
- 0
src/app/shared/components/footer/footer.component.html View 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>

+ 0
- 0
src/app/shared/components/footer/footer.component.scss View File


+ 25
- 0
src/app/shared/components/footer/footer.component.spec.ts View 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
- 0
src/app/shared/components/footer/footer.component.ts View 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() {
}
}

+ 75
- 0
src/app/shared/components/header-side/header-side.component.ts View File

@ -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)
}
}

+ 77
- 0
src/app/shared/components/header-side/header-side.template.html View File

@ -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>

+ 138
- 0
src/app/shared/components/layouts/admin-layout/admin-layout.component.ts View File

@ -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'
}
}
}

+ 66
- 0
src/app/shared/components/layouts/admin-layout/admin-layout.template.html View File

@ -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>

+ 1
- 0
src/app/shared/components/layouts/auth-layout/auth-layout.component.html View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

+ 14
- 0
src/app/shared/components/layouts/auth-layout/auth-layout.component.ts View File

@ -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() {
}
}

+ 18
- 0
src/app/shared/components/notifications/notifications.component.html View File

@ -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>

+ 46
- 0
src/app/shared/components/notifications/notifications.component.ts View File

@ -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
- 0
src/app/shared/components/shared-components.module.ts View 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 {}

+ 95
- 0
src/app/shared/components/sidebar-side/sidebar-side.component.html View File

@ -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>

+ 48
- 0
src/app/shared/components/sidebar-side/sidebar-side.component.ts View File

@ -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
- 0
src/app/shared/components/sidenav/sidenav.component.ts View 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
- 0
src/app/shared/components/sidenav/sidenav.template.html View 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
- 0
src/app/shared/directives/dropdown-anchor.directive.ts View 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
- 0
src/app/shared/directives/dropdown-link.directive.ts View 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
- 0
src/app/shared/directives/dropdown.directive.ts View 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
- 0
src/app/shared/directives/font-size.directive.ts View 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
- 0
src/app/shared/directives/matx-highlight.directive.ts View 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
- 0
src/app/shared/directives/matx-side-nav-toggle.directive.ts View 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();
});
}
}

+ 91
- 0
src/app/shared/directives/matx-sidenav-helper/matx-sidenav-helper.directive.ts View File

@ -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();
}
}

+ 21
- 0
src/app/shared/directives/matx-sidenav-helper/matx-sidenav-helper.service.ts View File

@ -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
- 0
src/app/shared/directives/scroll-to.directive.ts View 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
- 0
src/app/shared/directives/shared-directives.module.ts View 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
- 0
src/app/shared/guards/auth.guard.ts View 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
- 0
src/app/shared/guards/user-role.guard.ts View 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
- 0
src/app/shared/helpers/url.helper.ts View 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
- 0
src/app/shared/helpers/utils.ts View 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 changed in this diff

Loading…
Cancel
Save