├── .bowerrc ├── .gitignore ├── .jscsrc ├── .vscode └── settings.json ├── README.md ├── appveyor.yml ├── assets └── css │ └── styles.css ├── bower.json ├── favicon.ico ├── gulp.config.js ├── gulpfile.js ├── index.html ├── licence ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── home │ │ ├── home.component.html │ │ └── home.component.ts │ ├── main.ts │ ├── rxjs-operators.ts │ ├── schedules │ │ ├── schedule-edit.component.html │ │ ├── schedule-edit.component.ts │ │ ├── schedule-list.component.html │ │ └── schedule-list.component.ts │ ├── shared │ │ ├── directives │ │ │ ├── highlight.directive.ts │ │ │ └── mobile-hide.directive.ts │ │ ├── interfaces.ts │ │ ├── pipes │ │ │ └── date-format.pipe.ts │ │ ├── services │ │ │ └── data.service.ts │ │ └── utils │ │ │ ├── config.service.ts │ │ │ ├── items.service.ts │ │ │ ├── lower-case-url-serializer.ts │ │ │ ├── mapping.service.ts │ │ │ └── notification.service.ts │ └── users │ │ ├── user-card.component.html │ │ ├── user-card.component.ts │ │ ├── user-list.component.html │ │ └── user-list.component.ts └── server │ ├── index.js │ └── package.json ├── systemjs.config.js └── tsconfig.json /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/app/**/*.js 2 | src/app/**/*.map 3 | node_modules 4 | bower_components 5 | typings 6 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "excludeFiles": [ 3 | "node_modules/**", 4 | "bower_components/**", 5 | ".jscsrc", 6 | "build/js/*.js"], 7 | 8 | "requireCurlyBraces": [ 9 | "if", 10 | "else", 11 | "for", 12 | "while", 13 | "do", 14 | "try", 15 | "catch" 16 | ], 17 | "requireOperatorBeforeLineBreak": true, 18 | "requireCamelCaseOrUpperCaseIdentifiers": true, 19 | "maximumLineLength": { 20 | "value": 100, 21 | "allowComments": true, 22 | "allowRegex": true 23 | }, 24 | "validateIndentation": 4, 25 | "validateQuoteMarks": "'", 26 | 27 | "disallowMultipleLineStrings": true, 28 | "disallowMixedSpacesAndTabs": true, 29 | "disallowTrailingWhitespace": true, 30 | "disallowSpaceAfterPrefixUnaryOperators": true, 31 | "disallowMultipleVarDecl": null, 32 | 33 | "requireSpaceAfterKeywords": [ 34 | "if", 35 | "else", 36 | "for", 37 | "while", 38 | "do", 39 | "switch", 40 | "return", 41 | "try", 42 | "catch" 43 | ], 44 | "requireSpaceBeforeBinaryOperators": [ 45 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 46 | "&=", "|=", "^=", "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 47 | "|", "^", "&&", "||", "===", "==", ">=", 48 | "<=", "<", ">", "!=", "!==" 49 | ], 50 | "requireSpaceAfterBinaryOperators": true, 51 | "requireSpacesInConditionalExpression": true, 52 | "requireSpaceBeforeBlockStatements": true, 53 | "requireLineFeedAtFileEnd": true, 54 | "disallowSpacesInsideObjectBrackets": false, 55 | "disallowSpacesInsideArrayBrackets": "all", 56 | "disallowSpacesInsideParentheses": true, 57 | 58 | "jsDoc": { 59 | "checkParamNames": true, 60 | "requireParamTypes": true 61 | }, 62 | 63 | "disallowMultipleLineBreaks": true, 64 | 65 | "disallowCommaBeforeLineBreak": null, 66 | "disallowDanglingUnderscores": null, 67 | "disallowEmptyBlocks": null, 68 | "disallowTrailingComma": null, 69 | "requireCommaBeforeLineBreak": null, 70 | "requireDotNotation": null, 71 | "requireMultipleVarDecl": null, 72 | "requireParenthesesAroundIIFE": true 73 | } 74 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true, 6 | "**/app/**/*.js": true, 7 | "**/*.map": true 8 | }, 9 | "typescript.tsdk": "node_modules/typescript/lib" 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular CRUD ops, Modals, Animations, Pagination, DateTimePicker, Directives and much more.. 2 | [![Build status](https://ci.appveyor.com/api/projects/status/github/chsakell/angular2-features?branch=master&svg=true)](https://ci.appveyor.com/project/chsakell/angular2-features/branch/master) 3 |
4 | Read blog post
5 | 9 | It shows how to use several controls such as Modals, DateTimePicker, Pagination in Angular 4 applications. 10 | dotnet-core-api-14 11 |

Installation Instructions

12 |
    13 |
  1. Setup the API from here or read the accosiated post
  2. 14 |
  3. Clone the Scheduler.SPA app and open it in your favorite text editor
  4. 15 |
  5. Open a command prompt and run the following commands 16 |
      17 |
    1. npm install
    2. 18 |
    3. bower install
    4. 19 |
    20 |
  6. 21 |
  7. Start the API and set the _apiURI inside the utils/config.service.ts to point it
  8. 22 |
  9. Start the SPA by typing npm start
  10. 23 |
24 | 25 |

26 | 27 | angular-features-gif 28 | 29 |

30 |

Microsoft Azure Deployment

31 | Learn how to deploy an Angular 4 app on Microsoft Azure, here. 32 |

Donations

33 | For being part of open source projects and documenting my work here and on chsakell's blog I really do not charge anything. I try to avoid any type of ads also. 34 | 35 | If you think that any information you obtained here is worth of some money and are willing to pay for it, feel free to send any amount through paypal. 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 |
Paypal
42 | Buy me a beer 43 |
47 | 48 |

Follow chsakell's Blog

49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 69 | 70 | 71 |
FacebookTwitter
Microsoft Web Application Development
64 | facebook 65 | 67 | twitter-small 68 |
72 |

License

73 | Code released under the MIT license. 74 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "6" 4 | 5 | # Install scripts. (runs after repo cloning) 6 | install: 7 | # Get the latest stable version of Node.js or io.js 8 | - ps: Install-Product node $env:nodejs_version 9 | # install modules 10 | - npm install 11 | 12 | # Don't actually build. 13 | build: off 14 | -------------------------------------------------------------------------------- /assets/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 70px; /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */ 3 | background-color: whitesmoke; 4 | } 5 | 6 | .hero-spacer { 7 | margin-top: 50px; 8 | } 9 | 10 | .hero-feature { 11 | margin-bottom: 30px; 12 | } 13 | 14 | footer { 15 | margin: 50px 0; 16 | } 17 | 18 | .img-avatar { 19 | height: 150px; 20 | width: 150px; 21 | } 22 | 23 | .img-small { 24 | height: 50px; 25 | width: 50px; 26 | } 27 | 28 | .panel-heading h3 { 29 | white-space: nowrap; 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | line-height: normal; 33 | width: 75%; 34 | padding-top: 8px; 35 | } 36 | 37 | .navbar-inverse { 38 | background-color: #4765a0; 39 | border-color: whitesmoke; 40 | border: 2px solid whitesmoke; 41 | } 42 | 43 | .shadowCard { 44 | box-shadow: 10px 10px 5px #888888; 45 | } 46 | 47 | .ng-valid[required] { 48 | border-left: 5px solid #42A948; /* green */ 49 | } 50 | 51 | .ng-invalid:not(form) { 52 | border-left: 5px solid #a94442; /* red */ 53 | } 54 | 55 | .table>tbody>tr>td { 56 | vertical-align: middle; 57 | } 58 | 59 | .fa { 60 | padding-right: 5px; 61 | } 62 | 63 | .navbar-fixed-bottom { 64 | background-color: #1d2140; 65 | color: whitesmoke; 66 | } 67 | 68 | .carousel-indicators .active { 69 | background: #31708f; 70 | } 71 | .content { 72 | margin-top: 20px; 73 | } 74 | .adjust1 { 75 | float: left; 76 | width: 100%; 77 | margin-bottom: 0; 78 | } 79 | .adjust2 { 80 | margin: 0; 81 | } 82 | .carousel-indicators li { 83 | border: 1px solid #ccc; 84 | } 85 | .carousel-control { 86 | color: #31708f; 87 | width: 5%; 88 | } 89 | .carousel-control:hover, 90 | .carousel-control:focus { 91 | color: #31708f; 92 | } 93 | .carousel-control.left, 94 | .carousel-control.right { 95 | background-image: none; 96 | } 97 | .media-object { 98 | margin: auto; 99 | margin-top: 15%; 100 | } 101 | @media screen and (max-width: 768px) { 102 | .media-object { 103 | margin-top: 0; 104 | } 105 | } 106 | 107 | .loader { 108 | border: 16px solid #f3f3f3; 109 | border-radius: 50%; 110 | border-top: 16px solid #3498db; 111 | width: 120px; 112 | height: 120px; 113 | -webkit-animation: spin 2s linear infinite; 114 | animation: spin 2s linear infinite; 115 | margin: auto; 116 | position: absolute; 117 | top: 0; left: 0; bottom: 0; right: 0; 118 | } 119 | 120 | @-webkit-keyframes spin { 121 | 0% { -webkit-transform: rotate(0deg); } 122 | 100% { -webkit-transform: rotate(360deg); } 123 | } 124 | 125 | @keyframes spin { 126 | 0% { transform: rotate(0deg); } 127 | 100% { transform: rotate(360deg); } 128 | } -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scheduler.spa", 3 | "private": true, 4 | "dependencies": { 5 | "alertify.js" : "0.3.11", 6 | "bootstrap": "^3.3.6", 7 | "font-awesome": "latest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chsakell/angular2-features/69d4819e973ed8053e3fd8831ae015e19c72d4cf/favicon.ico -------------------------------------------------------------------------------- /gulp.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var base = 'wwwroot/'; 3 | 4 | var config = { 5 | base: base, 6 | compiledTs: 'app/**/*.js', 7 | dist: 'dist', 8 | index: base + 'index.html', 9 | ngTemplates: 'app/**/*.html', 10 | sourceMaps: 'app/**/*.map', 11 | tsFiles: 'app/**/*.ts', 12 | tsConfig: 'tsconfig.json', 13 | vendors: base + 'vendors' 14 | }; 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var gulp = require("gulp"); 3 | var del = require("del"); 4 | var replace = require("gulp-replace"); 5 | var sourcemaps = require('gulp-sourcemaps'); 6 | 7 | /** 8 | * Remove build directory. 9 | */ 10 | gulp.task('clean', function (cb) { 11 | return del(["build"], cb); 12 | }); 13 | 14 | /** 15 | * Copy all resources that are not TypeScript files into build directory. 16 | */ 17 | gulp.task("resources", ["server", "app", "assets","systemjs", "bower"], function () { 18 | console.log("Building resources..."); 19 | }); 20 | /* copy the app core files to the build folder */ 21 | gulp.task("app", ['index'], function(){ 22 | return gulp.src(["src/app/**", "!src/app/**/*.ts"]) 23 | .pipe(gulp.dest("build/app")); 24 | }); 25 | /* get the index file to the root of the build */ 26 | gulp.task("index", function(){ 27 | return gulp.src(["index.html"]) 28 | .pipe(replace('node_modules', 'lib')) 29 | .pipe(gulp.dest("build")); 30 | }); 31 | /* copy node server to build folder */ 32 | gulp.task("server", function () { 33 | return gulp.src(["index.js", "package.json"], { cwd: "src/server/**" }) 34 | .pipe(gulp.dest("build")); 35 | }); 36 | /* styles and other assets */ 37 | gulp.task("assets", function(){ 38 | return gulp.src(["assets/**/*"], { "base" : "." }) 39 | .pipe(gulp.dest("build")); 40 | }); 41 | 42 | /* styles and other assets */ 43 | gulp.task("systemjs", function(){ 44 | return gulp.src(["systemjs.config.js"]) 45 | .pipe(replace('node_modules', 'lib')) 46 | .pipe(replace('src/app', 'app')) 47 | .pipe(gulp.dest("build")); 48 | }); 49 | 50 | gulp.task("bower", function(){ 51 | return gulp.src(["bower.json"]) 52 | .pipe(gulp.dest("build")); 53 | }); 54 | 55 | /** 56 | * Copy all required libraries into build directory. 57 | */ 58 | gulp.task("libs", function () { 59 | var lib = "build/lib/"; 60 | 61 | gulp.src('node_modules/' + "@angular/**/*.js", 62 | { base: 'node_modules/' + "@angular/" }) 63 | .pipe(gulp.dest(lib + "@angular/")); 64 | 65 | gulp.src('node_modules/' + "angular2-in-memory-web-api/*.js", 66 | { base: 'node_modules/' }) 67 | .pipe(gulp.dest(lib)); 68 | 69 | gulp.src('node_modules/' + "core-js/client/shim*.js", 70 | { base: 'node_modules/' }) 71 | .pipe(gulp.dest(lib)); 72 | 73 | gulp.src('node_modules/' + "zone.js/dist/zone*.js", 74 | { base: 'node_modules/' }) 75 | .pipe(gulp.dest(lib)); 76 | 77 | gulp.src('node_modules/' + "reflect-metadata/Reflect*.js", 78 | { base: 'node_modules/' }) 79 | .pipe(gulp.dest(lib)); 80 | 81 | gulp.src('node_modules/' + "systemjs/dist/*.js", 82 | { base: 'node_modules/' }) 83 | .pipe(gulp.dest(lib)); 84 | 85 | gulp.src('node_modules/' + "rxjs/**/*.js", 86 | { base: 'node_modules/' }) 87 | .pipe(gulp.dest(lib)); 88 | 89 | gulp.src('node_modules/' + "bootstrap/dist/**/*", 90 | { base: 'node_modules/' }) 91 | .pipe(gulp.dest(lib)); 92 | 93 | gulp.src('node_modules/' + "ngx-bootstrap/**/*", 94 | { base: 'node_modules/' }) 95 | .pipe(gulp.dest(lib)); 96 | 97 | gulp.src('node_modules/' + "ng2-slim-loading-bar/**/*", 98 | { base: 'node_modules/' }) 99 | .pipe(gulp.dest(lib)); 100 | 101 | gulp.src('node_modules/' + "lodash/**/*", 102 | { base: 'node_modules/' }) 103 | .pipe(gulp.dest(lib)); 104 | 105 | gulp.src('node_modules/' + "moment/**/*", 106 | { base: 'node_modules/' }) 107 | .pipe(gulp.dest(lib)); 108 | }); 109 | /** 110 | * Build the project. 111 | */ 112 | gulp.task("default", ['resources', 'libs'], function () { 113 | console.log("Building the project ..."); 114 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Scheduler 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /licence: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Christos Sakellarios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "name": "scheduler", 4 | "author": "Chris Sakellarios", 5 | "license": "MIT", 6 | "repository": "https://github.com/chsakell/angular2-features", 7 | "private": true, 8 | "dependencies": { 9 | "@angular/animations": "4.0.2", 10 | "@angular/common": "4.0.2", 11 | "@angular/compiler": "4.0.2", 12 | "@angular/core": "4.0.2", 13 | "@angular/forms": "4.0.2", 14 | "@angular/http": "4.0.2", 15 | "@angular/platform-browser": "4.0.2", 16 | "@angular/platform-browser-dynamic": "4.0.2", 17 | "@angular/router": "4.0.2", 18 | "angular-in-memory-web-api": "~0.2.4", 19 | "bootstrap": "^3.3.6", 20 | "core-js": "^2.4.1", 21 | "jquery": "^3.0.0", 22 | "lodash": "^4.17.4", 23 | "@types/lodash": "^4.14.68", 24 | "moment": "^2.13.0", 25 | "ngx-bootstrap": "^1.7.1", 26 | "ng2-slim-loading-bar": "1.5.1", 27 | "reflect-metadata": "^0.1.8", 28 | "rxjs": "5.0.1", 29 | "systemjs": "0.19.40", 30 | "zone.js": "^0.8.5" 31 | }, 32 | "devDependencies": { 33 | "concurrently": "^3.0.0", 34 | "gulp": "3.9.1", 35 | "gulp-replace": "0.5.4", 36 | "del": "2.2.2", 37 | "gulp-sourcemaps": "2.2.0", 38 | "lite-server": "^2.2.0", 39 | "typescript": "2.2.2", 40 | "typings": "^1.3.2", 41 | "tslint": "^3.10.2", 42 | "@types/node": "~6.0.60" 43 | }, 44 | "scripts": { 45 | "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite --baseDir ./app --port 8000\" ", 46 | "lite": "lite-server", 47 | "tsc": "tsc", 48 | "tsc:w": "tsc -w", 49 | "typings": "typings" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |  2 | 57 |
58 | 59 |
60 | 61 |
62 | 70 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewContainerRef } from '@angular/core'; 2 | 3 | // Add the RxJS Observable operators we need in this app. 4 | import './rxjs-operators'; 5 | 6 | @Component({ 7 | moduleId: module.id, 8 | selector: 'scheduler', 9 | templateUrl: 'app.component.html' 10 | }) 11 | export class AppComponent { 12 | 13 | constructor(private viewContainerRef: ViewContainerRef) { 14 | // You need this small hack in order to catch application root view container ref 15 | this.viewContainerRef = viewContainerRef; 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import './rxjs-operators'; 2 | 3 | import { NgModule } from '@angular/core'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { FormsModule } from '@angular/forms'; 6 | import { HttpModule } from '@angular/http'; 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 8 | 9 | import { PaginationModule } from 'ngx-bootstrap'; 10 | import { DatepickerModule } from 'ngx-bootstrap'; 11 | import { ModalModule } from 'ngx-bootstrap'; 12 | import { ProgressbarModule } from 'ngx-bootstrap'; 13 | import { SlimLoadingBarService, SlimLoadingBarComponent } from 'ng2-slim-loading-bar'; 14 | import { TimepickerModule } from 'ngx-bootstrap'; 15 | 16 | import { AppComponent } from './app.component'; 17 | import { DateFormatPipe } from './shared/pipes/date-format.pipe'; 18 | import { HighlightDirective } from './shared/directives/highlight.directive'; 19 | import { HomeComponent } from './home/home.component'; 20 | import { MobileHideDirective } from './shared/directives/mobile-hide.directive'; 21 | import { ScheduleEditComponent } from './schedules/schedule-edit.component'; 22 | import { ScheduleListComponent } from './schedules/schedule-list.component'; 23 | import { UserCardComponent } from './users/user-card.component'; 24 | import { UserListComponent } from './users/user-list.component'; 25 | import { routing } from './app.routes'; 26 | 27 | import { DataService } from './shared/services/data.service'; 28 | import { ConfigService } from './shared/utils/config.service'; 29 | import { ItemsService } from './shared/utils/items.service'; 30 | import { MappingService } from './shared/utils/mapping.service'; 31 | import { NotificationService } from './shared/utils/notification.service'; 32 | import { UrlSerializer } from '@angular/router'; 33 | import { LowerCaseUrlSerializer } from './shared/utils/lower-case-url-serializer'; 34 | 35 | @NgModule({ 36 | imports: [ 37 | BrowserModule, 38 | BrowserAnimationsModule, 39 | DatepickerModule.forRoot(), 40 | FormsModule, 41 | HttpModule, 42 | ModalModule.forRoot(), 43 | ProgressbarModule.forRoot(), 44 | PaginationModule.forRoot(), 45 | routing, 46 | TimepickerModule.forRoot() 47 | ], 48 | declarations: [ 49 | AppComponent, 50 | DateFormatPipe, 51 | HighlightDirective, 52 | HomeComponent, 53 | MobileHideDirective, 54 | ScheduleEditComponent, 55 | ScheduleListComponent, 56 | SlimLoadingBarComponent, 57 | UserCardComponent, 58 | UserListComponent 59 | ], 60 | providers: [ 61 | ConfigService, 62 | DataService, 63 | ItemsService, 64 | MappingService, 65 | NotificationService, 66 | SlimLoadingBarService, 67 | { 68 | provide: UrlSerializer, 69 | useClass: LowerCaseUrlSerializer 70 | } 71 | ], 72 | bootstrap: [AppComponent] 73 | }) 74 | export class AppModule { } 75 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { ModuleWithProviders } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home/home.component'; 5 | import { UserListComponent } from './users/user-list.component'; 6 | import { ScheduleListComponent } from './schedules/schedule-list.component'; 7 | import { ScheduleEditComponent } from './schedules/schedule-edit.component'; 8 | 9 | const appRoutes: Routes = [ 10 | { path: 'users', component: UserListComponent }, 11 | { path: 'schedules', component: ScheduleListComponent }, 12 | { path: 'schedules/:id/edit', component: ScheduleEditComponent }, 13 | { path: '', component: HomeComponent } 14 | ]; 15 | 16 | export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); -------------------------------------------------------------------------------- /src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 | 
2 | 3 |
4 | 89 |
90 | 91 |
92 | 93 |
94 |
95 |

Latest Features

96 |
97 |
98 | 99 | 100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 |
108 |

ASP.NET Core

109 |

ASP.NET Core is a significant redesign of ASP.NET.

110 |

111 | More.. 112 |

113 |
114 |
115 |
116 |
117 |
118 | 119 | 120 | 121 | 122 |
123 |

EF Core

124 |

A cross-platform version of Entity Framework.

125 |

126 | More.. 127 |

128 |
129 |
130 |
131 |
132 |
133 | 134 | 135 | 136 | 137 |
138 |

Angular

139 |

Angular is a platform for building mobile and desktop web apps.

140 |

141 | More.. 142 |

143 |
144 |
145 |
146 |
147 |
148 | 149 | 150 | 151 | 152 |
153 |

TypeScript

154 |

A free and open source programming language.

155 |

156 | More.. 157 |

158 |
159 |
160 |
161 |
162 | 163 |
164 | 165 | 172 |
-------------------------------------------------------------------------------- /src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, trigger, state, style, animate, transition } from '@angular/core'; 2 | 3 | declare let componentHandler: any; 4 | 5 | @Component({ 6 | moduleId: module.id, 7 | templateUrl: 'home.component.html', 8 | animations: [ 9 | trigger('flyInOut', [ 10 | state('in', style({ opacity: 1, transform: 'translateX(0)' })), 11 | transition('void => *', [ 12 | style({ 13 | opacity: 0, 14 | transform: 'translateX(-100%)' 15 | }), 16 | animate('0.6s ease-in') 17 | ]), 18 | transition('* => void', [ 19 | animate('0.2s 10 ease-out', style({ 20 | opacity: 0, 21 | transform: 'translateX(100%)' 22 | })) 23 | ]) 24 | ]) 25 | ] 26 | }) 27 | export class HomeComponent { 28 | 29 | constructor() { 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app.module'; 3 | 4 | platformBrowserDynamic().bootstrapModule(AppModule); -------------------------------------------------------------------------------- /src/app/rxjs-operators.ts: -------------------------------------------------------------------------------- 1 | // Statics 2 | import 'rxjs/add/observable/throw'; 3 | 4 | // Operators 5 | import 'rxjs/add/operator/catch'; 6 | import 'rxjs/add/operator/debounceTime'; 7 | import 'rxjs/add/operator/distinctUntilChanged'; 8 | import 'rxjs/add/operator/map'; 9 | import 'rxjs/add/operator/switchMap'; 10 | import 'rxjs/add/operator/toPromise'; -------------------------------------------------------------------------------- /src/app/schedules/schedule-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 6 | 10 | 14 | 17 |
18 | 19 | 21 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | 32 |
33 | 34 |
35 | 36 | 38 |
39 | 40 |
41 | 42 | 44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 52 | 53 | 54 |
55 | 56 |
57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 | 69 |
70 | 71 |
72 | 73 | 76 |
77 |
78 | 79 | 82 |
83 |
84 |
85 |
86 |
87 | 88 |
Attendes
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 105 | 106 | 107 | 110 | 111 | 112 |
NameProfession
103 | attendee.name 104 | {{attendee.name}}{{attendee.profession}} 108 | 109 |
113 |
114 |
-------------------------------------------------------------------------------- /src/app/schedules/schedule-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { NgForm } from '@angular/forms'; 4 | 5 | import { SlimLoadingBarService } from 'ng2-slim-loading-bar'; 6 | 7 | import { DataService } from '../shared/services/data.service'; 8 | import { ItemsService } from '../shared/utils/items.service'; 9 | import { NotificationService } from '../shared/utils/notification.service'; 10 | import { ConfigService } from '../shared/utils/config.service'; 11 | import { MappingService } from '../shared/utils/mapping.service'; 12 | import { ISchedule, IScheduleDetails, IUser } from '../shared/interfaces'; 13 | import { DateFormatPipe } from '../shared/pipes/date-format.pipe'; 14 | 15 | @Component({ 16 | moduleId: module.id, 17 | selector: 'app-schedule-edit', 18 | templateUrl: 'schedule-edit.component.html' 19 | }) 20 | export class ScheduleEditComponent implements OnInit { 21 | apiHost: string; 22 | id: number; 23 | schedule: IScheduleDetails; 24 | scheduleLoaded: boolean = false; 25 | statuses: string[]; 26 | types: string[]; 27 | private sub: any; 28 | 29 | constructor(private route: ActivatedRoute, 30 | private router: Router, 31 | private dataService: DataService, 32 | private itemsService: ItemsService, 33 | private notificationService: NotificationService, 34 | private configService: ConfigService, 35 | private mappingService: MappingService, 36 | private loadingBarService:SlimLoadingBarService) { } 37 | 38 | ngOnInit() { 39 | // (+) converts string 'id' to a number 40 | this.id = +this.route.snapshot.params['id']; 41 | this.apiHost = this.configService.getApiHost(); 42 | this.loadScheduleDetails(); 43 | } 44 | 45 | loadScheduleDetails() { 46 | this.loadingBarService.start(); 47 | this.dataService.getScheduleDetails(this.id) 48 | .subscribe((schedule: IScheduleDetails) => { 49 | this.schedule = this.itemsService.getSerialized(schedule); 50 | this.scheduleLoaded = true; 51 | // Convert date times to readable format 52 | this.schedule.timeStart = new Date(this.schedule.timeStart.toString()); // new DateFormatPipe().transform(schedule.timeStart, ['local']); 53 | this.schedule.timeEnd = new Date(this.schedule.timeEnd.toString()); //new DateFormatPipe().transform(schedule.timeEnd, ['local']); 54 | this.statuses = this.schedule.statuses; 55 | this.types = this.schedule.types; 56 | 57 | this.loadingBarService.complete(); 58 | }, 59 | error => { 60 | this.loadingBarService.complete(); 61 | this.notificationService.printErrorMessage('Failed to load schedule. ' + error); 62 | }); 63 | } 64 | 65 | updateSchedule(editScheduleForm: NgForm) { 66 | console.log(editScheduleForm.value); 67 | 68 | var scheduleMapped = this.mappingService.mapScheduleDetailsToSchedule(this.schedule); 69 | 70 | this.loadingBarService.start(); 71 | this.dataService.updateSchedule(scheduleMapped) 72 | .subscribe(() => { 73 | this.notificationService.printSuccessMessage('Schedule has been updated'); 74 | this.loadingBarService.complete(); 75 | }, 76 | error => { 77 | this.loadingBarService.complete(); 78 | this.notificationService.printErrorMessage('Failed to update schedule. ' + error); 79 | }); 80 | } 81 | 82 | removeAttendee(attendee: IUser) { 83 | this.notificationService.openConfirmationDialog('Are you sure you want to remove ' 84 | + attendee.name + ' from this schedule?', 85 | () => { 86 | this.loadingBarService.start(); 87 | this.dataService.deleteScheduleAttendee(this.schedule.id, attendee.id) 88 | .subscribe(() => { 89 | this.itemsService.removeItemFromArray(this.schedule.attendees, attendee); 90 | this.notificationService.printSuccessMessage(attendee.name + ' will not attend the schedule.'); 91 | this.loadingBarService.complete(); 92 | }, 93 | error => { 94 | this.loadingBarService.complete(); 95 | this.notificationService.printErrorMessage('Failed to remove ' + attendee.name + ' ' + error); 96 | }); 97 | }); 98 | } 99 | 100 | back() { 101 | this.router.navigate(['/schedules']); 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/app/schedules/schedule-list.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 38 | 39 | 40 |
TitleCreatorDescriptionTime StartTime End
{{schedule.title}}{{schedule.creator}}{{schedule.description}}{{schedule.location}}{{schedule.timeStart | dateFormat | date:'medium'}}{{schedule.timeEnd | dateFormat | date:'medium'}} 33 | Edit 36 | 37 |
41 | 42 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /src/app/schedules/schedule-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, Input, Output, 2 | trigger, 3 | state, 4 | style, 5 | animate, 6 | transition } from '@angular/core'; 7 | 8 | import { ModalDirective } from 'ngx-bootstrap'; 9 | import { SlimLoadingBarService } from 'ng2-slim-loading-bar'; 10 | 11 | import { DataService } from '../shared/services/data.service'; 12 | import { DateFormatPipe } from '../shared/pipes/date-format.pipe'; 13 | import { ItemsService } from '../shared/utils/items.service'; 14 | import { NotificationService } from '../shared/utils/notification.service'; 15 | import { ConfigService } from '../shared/utils/config.service'; 16 | import { ISchedule, IScheduleDetails, Pagination, PaginatedResult } from '../shared/interfaces'; 17 | 18 | @Component({ 19 | moduleId: module.id, 20 | selector: 'app-schedules', 21 | templateUrl: 'schedule-list.component.html', 22 | animations: [ 23 | trigger('flyInOut', [ 24 | state('in', style({ opacity: 1, transform: 'translateX(0)' })), 25 | transition('void => *', [ 26 | style({ 27 | opacity: 0, 28 | transform: 'translateX(-100%)' 29 | }), 30 | animate('0.5s ease-in') 31 | ]), 32 | transition('* => void', [ 33 | animate('0.2s 10 ease-out', style({ 34 | opacity: 0, 35 | transform: 'translateX(100%)' 36 | })) 37 | ]) 38 | ]) 39 | ] 40 | }) 41 | export class ScheduleListComponent implements OnInit { 42 | @ViewChild('childModal') public childModal: ModalDirective; 43 | schedules: ISchedule[]; 44 | apiHost: string; 45 | 46 | public itemsPerPage: number = 2; 47 | public totalItems: number = 0; 48 | public currentPage: number = 1; 49 | 50 | // Modal properties 51 | @ViewChild('modal') 52 | modal: any; 53 | items: string[] = ['item1', 'item2', 'item3']; 54 | selected: string; 55 | output: string; 56 | selectedScheduleId: number; 57 | scheduleDetails: IScheduleDetails; 58 | selectedScheduleLoaded: boolean = false; 59 | index: number = 0; 60 | backdropOptions = [true, false, 'static']; 61 | animation: boolean = true; 62 | keyboard: boolean = true; 63 | backdrop: string | boolean = true; 64 | 65 | constructor( 66 | private dataService: DataService, 67 | private itemsService: ItemsService, 68 | private notificationService: NotificationService, 69 | private configService: ConfigService, 70 | private loadingBarService:SlimLoadingBarService) { } 71 | 72 | ngOnInit() { 73 | this.apiHost = this.configService.getApiHost(); 74 | this.loadSchedules(); 75 | } 76 | 77 | loadSchedules() { 78 | this.loadingBarService.start(); 79 | 80 | this.dataService.getSchedules(this.currentPage, this.itemsPerPage) 81 | .subscribe((res: PaginatedResult) => { 82 | this.schedules = res.result;// schedules; 83 | this.totalItems = res.pagination.TotalItems; 84 | this.loadingBarService.complete(); 85 | }, 86 | error => { 87 | this.loadingBarService.complete(); 88 | this.notificationService.printErrorMessage('Failed to load schedules. ' + error); 89 | }); 90 | } 91 | 92 | pageChanged(event: any): void { 93 | this.currentPage = event.page; 94 | this.loadSchedules(); 95 | //console.log('Page changed to: ' + event.page); 96 | //console.log('Number items per page: ' + event.itemsPerPage); 97 | }; 98 | 99 | removeSchedule(schedule: ISchedule) { 100 | this.notificationService.openConfirmationDialog('Are you sure you want to delete this schedule?', 101 | () => { 102 | this.loadingBarService.start(); 103 | this.dataService.deleteSchedule(schedule.id) 104 | .subscribe(() => { 105 | this.itemsService.removeItemFromArray(this.schedules, schedule); 106 | this.notificationService.printSuccessMessage(schedule.title + ' has been deleted.'); 107 | this.loadingBarService.complete(); 108 | }, 109 | error => { 110 | this.loadingBarService.complete(); 111 | this.notificationService.printErrorMessage('Failed to delete ' + schedule.title + ' ' + error); 112 | }); 113 | }); 114 | } 115 | 116 | viewScheduleDetails(id: number) { 117 | this.selectedScheduleId = id; 118 | 119 | this.dataService.getScheduleDetails(this.selectedScheduleId) 120 | .subscribe((schedule: IScheduleDetails) => { 121 | this.scheduleDetails = this.itemsService.getSerialized(schedule); 122 | // Convert date times to readable format 123 | this.scheduleDetails.timeStart = new DateFormatPipe().transform(schedule.timeStart, ['local']); 124 | this.scheduleDetails.timeEnd = new DateFormatPipe().transform(schedule.timeEnd, ['local']); 125 | this.loadingBarService.complete(); 126 | this.selectedScheduleLoaded = true; 127 | this.childModal.show();//.open('lg'); 128 | }, 129 | error => { 130 | this.loadingBarService.complete(); 131 | this.notificationService.printErrorMessage('Failed to load schedule. ' + error); 132 | }); 133 | } 134 | 135 | public hideChildModal(): void { 136 | this.childModal.hide(); 137 | } 138 | } -------------------------------------------------------------------------------- /src/app/shared/directives/highlight.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; 2 | @Directive({ 3 | selector: '[highlight]' 4 | }) 5 | export class HighlightDirective { 6 | private _defaultColor = 'beige'; 7 | private el: HTMLElement; 8 | 9 | constructor(el: ElementRef) { 10 | this.el = el.nativeElement; 11 | } 12 | 13 | @Input('highlight') highlightColor: string; 14 | 15 | @HostListener('mouseenter') onMouseEnter() { 16 | this.highlight(this.highlightColor || this._defaultColor); 17 | } 18 | @HostListener('mouseleave') onMouseLeave() { 19 | this.highlight(null); 20 | } 21 | 22 | private highlight(color: string) { 23 | this.el.style.backgroundColor = color; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/shared/directives/mobile-hide.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, HostListener, Input } from '@angular/core'; 2 | @Directive({ 3 | selector: '[mobileHide]', 4 | host: { 5 | '(window:resize)': 'onResize($event)' 6 | } 7 | }) 8 | export class MobileHideDirective { 9 | private _defaultMaxWidth: number = 768; 10 | private el: HTMLElement; 11 | 12 | constructor(el: ElementRef) { 13 | this.el = el.nativeElement; 14 | } 15 | 16 | @Input('mobileHide') mobileHide: number; 17 | 18 | onResize(event:Event) { 19 | var window : any = event.target; 20 | var currentWidth = window.innerWidth; 21 | if(currentWidth < (this.mobileHide || this._defaultMaxWidth)) 22 | { 23 | this.el.style.display = 'none'; 24 | } 25 | else 26 | { 27 | this.el.style.display = 'block'; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IUser { 2 | id: number; 3 | name: string; 4 | avatar: string; 5 | profession: string; 6 | schedulesCreated: number; 7 | } 8 | 9 | export interface ISchedule { 10 | id: number; 11 | title: string; 12 | description: string; 13 | timeStart: Date; 14 | timeEnd: Date; 15 | location: string; 16 | type: string; 17 | status: string; 18 | dateCreated: Date; 19 | dateUpdated: Date; 20 | creator: string; 21 | creatorId: number; 22 | attendees: number[]; 23 | } 24 | 25 | export interface IScheduleDetails { 26 | id: number; 27 | title: string; 28 | description: string; 29 | timeStart: Date; 30 | timeEnd: Date; 31 | location: string; 32 | type: string; 33 | status: string; 34 | dateCreated: Date; 35 | dateUpdated: Date; 36 | creator: string; 37 | creatorId: number; 38 | attendees: IUser[]; 39 | statuses: string[]; 40 | types: string[]; 41 | } 42 | 43 | export interface Pagination { 44 | CurrentPage : number; 45 | ItemsPerPage : number; 46 | TotalItems : number; 47 | TotalPages: number; 48 | } 49 | 50 | export class PaginatedResult { 51 | result : T; 52 | pagination : Pagination; 53 | } 54 | 55 | export interface Predicate { 56 | (item: T): boolean 57 | } -------------------------------------------------------------------------------- /src/app/shared/pipes/date-format.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'dateFormat' 5 | }) 6 | 7 | export class DateFormatPipe implements PipeTransform { 8 | transform(value: any, args: any[]): any { 9 | 10 | if (args && args[0] === 'local') { 11 | return new Date(value).toLocaleString(); 12 | } 13 | else if (value) { 14 | return new Date(value); 15 | } 16 | return value; 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/shared/services/data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Http, Response, Headers } from '@angular/http'; 3 | //Grab everything with import 'rxjs/Rx'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import {Observer} from 'rxjs/Observer'; 6 | import 'rxjs/add/operator/map'; 7 | import 'rxjs/add/operator/catch'; 8 | 9 | import { IUser, ISchedule, IScheduleDetails, Pagination, PaginatedResult } from '../interfaces'; 10 | import { ItemsService } from '../utils/items.service'; 11 | import { ConfigService } from '../utils/config.service'; 12 | 13 | @Injectable() 14 | export class DataService { 15 | 16 | _baseUrl: string = ''; 17 | 18 | constructor(private http: Http, 19 | private itemsService: ItemsService, 20 | private configService: ConfigService) { 21 | this._baseUrl = configService.getApiURI(); 22 | } 23 | 24 | getUsers(): Observable { 25 | return this.http.get(this._baseUrl + 'users') 26 | .map((res: Response) => { 27 | return res.json(); 28 | }) 29 | .catch(this.handleError); 30 | } 31 | 32 | getUserSchedules(id: number): Observable { 33 | return this.http.get(this._baseUrl + 'users/' + id + '/schedules') 34 | .map((res: Response) => { 35 | return res.json(); 36 | }) 37 | .catch(this.handleError); 38 | } 39 | 40 | createUser(user: IUser): Observable { 41 | 42 | let headers = new Headers(); 43 | headers.append('Content-Type', 'application/json'); 44 | 45 | return this.http.post(this._baseUrl + 'users/', JSON.stringify(user), { 46 | headers: headers 47 | }) 48 | .map((res: Response) => { 49 | return res.json(); 50 | }) 51 | .catch(this.handleError); 52 | } 53 | 54 | updateUser(user: IUser): Observable { 55 | 56 | let headers = new Headers(); 57 | headers.append('Content-Type', 'application/json'); 58 | 59 | return this.http.put(this._baseUrl + 'users/' + user.id, JSON.stringify(user), { 60 | headers: headers 61 | }) 62 | .map((res: Response) => { 63 | return; 64 | }) 65 | .catch(this.handleError); 66 | } 67 | 68 | deleteUser(id: number): Observable { 69 | return this.http.delete(this._baseUrl + 'users/' + id) 70 | .map((res: Response) => { 71 | return; 72 | }) 73 | .catch(this.handleError); 74 | } 75 | /* 76 | getSchedules(page?: number, itemsPerPage?: number): Observable { 77 | let headers = new Headers(); 78 | if (page != null && itemsPerPage != null) { 79 | headers.append('Pagination', page + ',' + itemsPerPage); 80 | } 81 | 82 | return this.http.get(this._baseUrl + 'schedules', { 83 | headers: headers 84 | }) 85 | .map((res: Response) => { 86 | return res.json(); 87 | }) 88 | .catch(this.handleError); 89 | } 90 | */ 91 | 92 | getSchedules(page?: number, itemsPerPage?: number): Observable> { 93 | var peginatedResult: PaginatedResult = new PaginatedResult(); 94 | 95 | let headers = new Headers(); 96 | if (page != null && itemsPerPage != null) { 97 | headers.append('Pagination', page + ',' + itemsPerPage); 98 | } 99 | 100 | return this.http.get(this._baseUrl + 'schedules', { 101 | headers: headers 102 | }) 103 | .map((res: Response) => { 104 | console.log(res.headers.keys()); 105 | peginatedResult.result = res.json(); 106 | 107 | if (res.headers.get("Pagination") != null) { 108 | //var pagination = JSON.parse(res.headers.get("Pagination")); 109 | var paginationHeader: Pagination = this.itemsService.getSerialized(JSON.parse(res.headers.get("Pagination"))); 110 | console.log(paginationHeader); 111 | peginatedResult.pagination = paginationHeader; 112 | } 113 | return peginatedResult; 114 | }) 115 | .catch(this.handleError); 116 | } 117 | 118 | getSchedule(id: number): Observable { 119 | return this.http.get(this._baseUrl + 'schedules/' + id) 120 | .map((res: Response) => { 121 | return res.json(); 122 | }) 123 | .catch(this.handleError); 124 | } 125 | 126 | getScheduleDetails(id: number): Observable { 127 | return this.http.get(this._baseUrl + 'schedules/' + id + '/details') 128 | .map((res: Response) => { 129 | return res.json(); 130 | }) 131 | .catch(this.handleError); 132 | } 133 | 134 | updateSchedule(schedule: ISchedule): Observable { 135 | 136 | let headers = new Headers(); 137 | headers.append('Content-Type', 'application/json'); 138 | 139 | return this.http.put(this._baseUrl + 'schedules/' + schedule.id, JSON.stringify(schedule), { 140 | headers: headers 141 | }) 142 | .map((res: Response) => { 143 | return; 144 | }) 145 | .catch(this.handleError); 146 | } 147 | 148 | deleteSchedule(id: number): Observable { 149 | return this.http.delete(this._baseUrl + 'schedules/' + id) 150 | .map((res: Response) => { 151 | return; 152 | }) 153 | .catch(this.handleError); 154 | } 155 | 156 | deleteScheduleAttendee(id: number, attendee: number) { 157 | 158 | return this.http.delete(this._baseUrl + 'schedules/' + id + '/removeattendee/' + attendee) 159 | .map((res: Response) => { 160 | return; 161 | }) 162 | .catch(this.handleError); 163 | } 164 | 165 | private handleError(error: any) { 166 | var applicationError = error.headers.get('Application-Error'); 167 | var serverError = error.json(); 168 | var modelStateErrors: string = ''; 169 | 170 | if (!serverError.type) { 171 | console.log(serverError); 172 | for (var key in serverError) { 173 | if (serverError[key]) 174 | modelStateErrors += serverError[key] + '\n'; 175 | } 176 | } 177 | 178 | modelStateErrors = modelStateErrors = '' ? null : modelStateErrors; 179 | 180 | return Observable.throw(applicationError || modelStateErrors || 'Server error'); 181 | } 182 | } -------------------------------------------------------------------------------- /src/app/shared/utils/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class ConfigService { 5 | 6 | _apiURI : string; 7 | 8 | constructor() { 9 | this._apiURI = 'http://localhost:5000/api/'; 10 | } 11 | 12 | getApiURI() { 13 | return this._apiURI; 14 | } 15 | 16 | getApiHost() { 17 | return this._apiURI.replace('api/',''); 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/shared/utils/items.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Predicate } from '../interfaces' 3 | 4 | import * as _ from 'lodash'; 5 | 6 | @Injectable() 7 | export class ItemsService { 8 | 9 | constructor() { } 10 | 11 | /* 12 | Removes an item from an array using the lodash library 13 | */ 14 | removeItemFromArray(array: Array, item: any) { 15 | _.remove(array, function (current) { 16 | //console.log(current); 17 | return JSON.stringify(current) === JSON.stringify(item); 18 | }); 19 | } 20 | 21 | removeItems(array: Array, predicate: Predicate) { 22 | _.remove(array, predicate); 23 | } 24 | 25 | /* 26 | Finds a specific item in an array using a predicate and repsaces it 27 | */ 28 | setItem(array: Array, predicate: Predicate, item: T) { 29 | var _oldItem = _.find(array, predicate); 30 | if(_oldItem){ 31 | var index = _.indexOf(array, _oldItem); 32 | array.splice(index, 1, item); 33 | } else { 34 | array.push(item); 35 | } 36 | } 37 | 38 | /* 39 | Adds an item to zero index 40 | */ 41 | addItemToStart(array: Array, item: any) { 42 | array.splice(0, 0, item); 43 | } 44 | 45 | /* 46 | From an array of type T, select all values of type R for property 47 | */ 48 | getPropertyValues(array: Array, property : string) : R 49 | { 50 | var result = _.map(array, property); 51 | return result; 52 | } 53 | 54 | /* 55 | Util method to serialize a string to a specific Type 56 | */ 57 | getSerialized(arg: any): T { 58 | return JSON.parse(JSON.stringify(arg)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/app/shared/utils/lower-case-url-serializer.ts: -------------------------------------------------------------------------------- 1 | import { DefaultUrlSerializer, UrlTree } from '@angular/router'; 2 | // /ref: https://stackoverflow.com/questions/42065409/angular-2-routes-3-0-case-sensitive 3 | 4 | export class LowerCaseUrlSerializer extends DefaultUrlSerializer { 5 | parse(url: string): UrlTree { 6 | return super.parse(url.toLowerCase()); 7 | } 8 | } -------------------------------------------------------------------------------- /src/app/shared/utils/mapping.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | import { ISchedule, IScheduleDetails, IUser } from '../interfaces'; 4 | import { ItemsService } from './items.service' 5 | 6 | @Injectable() 7 | export class MappingService { 8 | 9 | constructor(private itemsService : ItemsService) { } 10 | 11 | mapScheduleDetailsToSchedule(scheduleDetails: IScheduleDetails): ISchedule { 12 | var schedule: ISchedule = { 13 | id: scheduleDetails.id, 14 | title: scheduleDetails.title, 15 | description: scheduleDetails.description, 16 | timeStart: scheduleDetails.timeStart, 17 | timeEnd: scheduleDetails.timeEnd, 18 | location: scheduleDetails.location, 19 | type: scheduleDetails.type, 20 | status: scheduleDetails.status, 21 | dateCreated: scheduleDetails.dateCreated, 22 | dateUpdated: scheduleDetails.dateUpdated, 23 | creator: scheduleDetails.creator, 24 | creatorId: scheduleDetails.creatorId, 25 | attendees: this.itemsService.getPropertyValues(scheduleDetails.attendees, 'id') 26 | } 27 | 28 | return schedule; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/app/shared/utils/notification.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Predicate } from '../interfaces' 3 | 4 | declare var alertify: any; 5 | 6 | @Injectable() 7 | export class NotificationService { 8 | private _notifier: any = alertify; 9 | 10 | constructor() { } 11 | 12 | /* 13 | Opens a confirmation dialog using the alertify.js lib 14 | */ 15 | openConfirmationDialog(message: string, okCallback: () => any) { 16 | this._notifier.confirm(message, function (e) { 17 | if (e) { 18 | okCallback(); 19 | } else { 20 | } 21 | }); 22 | } 23 | 24 | /* 25 | Prints a success message using the alertify.js lib 26 | */ 27 | printSuccessMessage(message: string) { 28 | 29 | this._notifier.success(message); 30 | } 31 | 32 | /* 33 | Prints an error message using the alertify.js lib 34 | */ 35 | printErrorMessage(message: string) { 36 | this._notifier.error(message); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/users/user-card.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

{{edittedUser.name}}

4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |

13 | {{edittedUser.profession}} 14 |

15 |

16 | 17 |

18 |

19 | 23 |

24 |
25 |
26 |
27 | 43 |
44 | 45 | -------------------------------------------------------------------------------- /src/app/users/user-card.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, OnInit, ViewContainerRef, EventEmitter, ViewChild, 2 | trigger, 3 | state, 4 | style, 5 | animate, 6 | transition } from '@angular/core'; 7 | 8 | import { IUser, ISchedule } from '../shared/interfaces'; 9 | import { DataService } from '../shared/services/data.service'; 10 | import { ItemsService } from '../shared/utils/items.service'; 11 | import { NotificationService } from '../shared/utils/notification.service'; 12 | import { ConfigService } from '../shared/utils/config.service'; 13 | import { HighlightDirective } from '../shared/directives/highlight.directive'; 14 | 15 | import { ModalDirective } from 'ngx-bootstrap'; 16 | 17 | @Component({ 18 | moduleId: module.id, 19 | selector: 'user-card', 20 | templateUrl: 'user-card.component.html', 21 | animations: [ 22 | trigger('flyInOut', [ 23 | state('in', style({ opacity: 1, transform: 'translateX(0)' })), 24 | transition('void => *', [ 25 | style({ 26 | opacity: 0, 27 | transform: 'translateX(-100%)' 28 | }), 29 | animate('0.5s ease-in') 30 | ]), 31 | transition('* => void', [ 32 | animate('0.2s 10 ease-out', style({ 33 | opacity: 0, 34 | transform: 'translateX(100%)' 35 | })) 36 | ]) 37 | ]) 38 | ] 39 | }) 40 | export class UserCardComponent implements OnInit { 41 | @ViewChild('childModal') public childModal: ModalDirective; 42 | @Input() user: IUser; 43 | @Output() removeUser = new EventEmitter(); 44 | @Output() userCreated = new EventEmitter(); 45 | 46 | edittedUser: IUser; 47 | onEdit: boolean = false; 48 | apiHost: string; 49 | // Modal properties 50 | @ViewChild('modal') 51 | modal: any; 52 | items: string[] = ['item1', 'item2', 'item3']; 53 | selected: string; 54 | output: string; 55 | userSchedules: ISchedule[]; 56 | userSchedulesLoaded: boolean = false; 57 | index: number = 0; 58 | backdropOptions = [true, false, 'static']; 59 | animation: boolean = true; 60 | keyboard: boolean = true; 61 | backdrop: string | boolean = true; 62 | 63 | constructor(private itemsService: ItemsService, 64 | private notificationService: NotificationService, 65 | private dataService: DataService, 66 | private configService: ConfigService) { } 67 | 68 | ngOnInit() { 69 | this.apiHost = this.configService.getApiHost(); 70 | this.edittedUser = this.itemsService.getSerialized(this.user); 71 | if (this.user.id < 0) 72 | this.editUser(); 73 | } 74 | 75 | editUser() { 76 | this.onEdit = !this.onEdit; 77 | this.edittedUser = this.itemsService.getSerialized(this.user); 78 | // JSON.parse(JSON.stringify(this.user)); // todo Utils.. 79 | } 80 | 81 | createUser() { 82 | //this.slimLoader.start(); 83 | this.dataService.createUser(this.edittedUser) 84 | .subscribe((userCreated) => { 85 | this.user = this.itemsService.getSerialized(userCreated); 86 | this.edittedUser = this.itemsService.getSerialized(this.user); 87 | this.onEdit = false; 88 | 89 | this.userCreated.emit({ value: userCreated }); 90 | //this.slimLoader.complete(); 91 | }, 92 | error => { 93 | this.notificationService.printErrorMessage('Failed to created user'); 94 | this.notificationService.printErrorMessage(error); 95 | //this.slimLoader.complete(); 96 | }); 97 | } 98 | 99 | updateUser() { 100 | //this.slimLoader.start(); 101 | this.dataService.updateUser(this.edittedUser) 102 | .subscribe(() => { 103 | this.user = this.edittedUser; 104 | this.onEdit = !this.onEdit; 105 | this.notificationService.printSuccessMessage(this.user.name + ' has been updated'); 106 | //this.slimLoader.complete(); 107 | }, 108 | error => { 109 | this.notificationService.printErrorMessage('Failed to edit user'); 110 | this.notificationService.printErrorMessage(error); 111 | //this.slimLoader.complete(); 112 | }); 113 | } 114 | 115 | openRemoveModal() { 116 | this.notificationService.openConfirmationDialog('Are you sure you want to remove ' 117 | + this.user.name + '?', 118 | () => { 119 | //this.slimLoader.start(); 120 | this.dataService.deleteUser(this.user.id) 121 | .subscribe( 122 | res => { 123 | this.removeUser.emit({ 124 | value: this.user 125 | }); 126 | //this.slimLoader.complete(); 127 | //this.slimLoader.complete(); 128 | }, error => { 129 | this.notificationService.printErrorMessage(error); 130 | //this.slimLoader.complete(); 131 | }) 132 | }); 133 | } 134 | 135 | viewSchedules(user: IUser) { 136 | console.log(user); 137 | this.dataService.getUserSchedules(this.edittedUser.id) 138 | .subscribe((schedules: ISchedule[]) => { 139 | this.userSchedules = schedules; 140 | console.log(this.userSchedules); 141 | this.userSchedulesLoaded = true; 142 | this.childModal.show(); 143 | //this.slimLoader.complete(); 144 | }, 145 | error => { 146 | //this.slimLoader.complete(); 147 | this.notificationService.printErrorMessage('Failed to load users. ' + error); 148 | }); 149 | 150 | } 151 | 152 | public hideChildModal(): void { 153 | this.childModal.hide(); 154 | } 155 | 156 | opened() { 157 | //this.slimLoader.start(); 158 | this.dataService.getUserSchedules(this.edittedUser.id) 159 | .subscribe((schedules: ISchedule[]) => { 160 | this.userSchedules = schedules; 161 | console.log(this.userSchedules); 162 | this.userSchedulesLoaded = true; 163 | //this.slimLoader.complete(); 164 | }, 165 | error => { 166 | //this.slimLoader.complete(); 167 | this.notificationService.printErrorMessage('Failed to load users. ' + error); 168 | }); 169 | this.output = '(opened)'; 170 | } 171 | 172 | isUserValid(): boolean { 173 | return !(this.edittedUser.name.trim() === "") 174 | && !(this.edittedUser.profession.trim() === ""); 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /src/app/users/user-list.component.html: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 |
7 | 8 |
9 |
10 | 11 |
12 |
-------------------------------------------------------------------------------- /src/app/users/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { DataService } from '../shared/services/data.service'; 4 | import { ItemsService } from '../shared/utils/items.service'; 5 | import { NotificationService } from '../shared/utils/notification.service'; 6 | import { IUser } from '../shared/interfaces'; 7 | import { UserCardComponent } from './user-card.component'; 8 | 9 | @Component({ 10 | moduleId: module.id, 11 | selector: 'users', 12 | templateUrl: 'user-list.component.html' 13 | }) 14 | export class UserListComponent implements OnInit { 15 | 16 | users: IUser[]; 17 | addingUser: boolean = false; 18 | 19 | constructor(private dataService: DataService, 20 | private itemsService: ItemsService, 21 | private notificationService: NotificationService) { } 22 | 23 | ngOnInit() { 24 | this.dataService.getUsers() 25 | .subscribe((users: IUser[]) => { 26 | this.users = users; 27 | }, 28 | error => { 29 | this.notificationService.printErrorMessage('Failed to load users. ' + error); 30 | }); 31 | } 32 | 33 | removeUser(user: any) { 34 | var _user: IUser = this.itemsService.getSerialized(user.value); 35 | this.itemsService.removeItemFromArray(this.users, _user); 36 | // inform user 37 | this.notificationService.printSuccessMessage(_user.name + ' has been removed'); 38 | } 39 | 40 | userCreated(user: any) { 41 | var _user: IUser = this.itemsService.getSerialized(user.value); 42 | this.addingUser = false; 43 | // inform user 44 | this.notificationService.printSuccessMessage(_user.name + ' has been created'); 45 | console.log(_user.id); 46 | this.itemsService.setItem(this.users, (u) => u.id == -1, _user); 47 | // todo fix user with id:-1 48 | } 49 | 50 | addUser() { 51 | this.addingUser = true; 52 | var newUser = { id: -1, name: '', avatar: 'avatar_05.png', profession: '', schedulesCreated: 0 }; 53 | this.itemsService.addItemToStart(this.users, newUser); 54 | //this.users.splice(0, 0, newUser); 55 | } 56 | 57 | cancelAddUser() { 58 | this.addingUser = false; 59 | this.itemsService.removeItems(this.users, x => x.id < 0); 60 | } 61 | } -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | path = require('path'), 3 | fs = require('fs'); 4 | 5 | var app = express(); 6 | var staticRoot = __dirname + '/'; 7 | 8 | app.set('port', (process.env.PORT || 3000)); 9 | 10 | app.use(express.static(staticRoot)); 11 | 12 | app.use(function(req, res, next){ 13 | 14 | // if the request is not html then move along 15 | var accept = req.accepts('html', 'json', 'xml'); 16 | if(accept !== 'html'){ 17 | return next(); 18 | } 19 | 20 | // if the request has a '.' assume that it's for a file, move along 21 | var ext = path.extname(req.path); 22 | if (ext !== ''){ 23 | return next(); 24 | } 25 | 26 | fs.createReadStream(staticRoot + 'index.html').pipe(res); 27 | 28 | }); 29 | 30 | app.listen(app.get('port'), function() { 31 | console.log('app running on port', app.get('port')); 32 | }); -------------------------------------------------------------------------------- /src/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scheduler-web-ui", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "express": "4.13.4" 6 | }, 7 | "scripts": { 8 | "postinstall": "bower install" 9 | } 10 | } -------------------------------------------------------------------------------- /systemjs.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * System configuration for Angular 2 samples 3 | * Adjust as necessary for your application needs. 4 | */ 5 | (function (global) { 6 | System.config({ 7 | paths: { 8 | // paths serve as alias 9 | 'npm:': 'node_modules/' 10 | }, 11 | // map tells the System loader where to look for things 12 | map: { 13 | // our app is within the app folder 14 | app: 'src/app', 15 | // angular bundles 16 | '@angular/animations': 'npm:@angular/animations/bundles/animations.umd.js', 17 | '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js', 18 | '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js', 19 | '@angular/core': 'npm:@angular/core/bundles/core.umd.js', 20 | '@angular/common': 'npm:@angular/common/bundles/common.umd.js', 21 | '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js', 22 | '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js', 23 | '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js', 24 | '@angular/http': 'npm:@angular/http/bundles/http.umd.js', 25 | '@angular/router': 'npm:@angular/router/bundles/router.umd.js', 26 | '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js', 27 | // other libraries 28 | 'rxjs': 'npm:rxjs', 29 | 'angular-in-memory-web-api': 'npm:angular-in-memory-web-api', 30 | 'jquery': 'npm:jquery/', 31 | 'lodash': 'npm:lodash/lodash.js', 32 | 'moment': 'npm:moment', 33 | 'ngx-bootstrap': 'npm:ngx-bootstrap', 34 | 'ng2-slim-loading-bar': 'npm:ng2-slim-loading-bar', 35 | 'symbol-observable': 'npm:symbol-observable' 36 | }, 37 | // packages tells the System loader how to load when no filename and/or no extension 38 | packages: { 39 | app: { 40 | main: './main.js', 41 | defaultExtension: 'js' 42 | }, 43 | rxjs: { 44 | defaultExtension: 'js' 45 | }, 46 | 'angular-in-memory-web-api': { 47 | main: './index.js', 48 | defaultExtension: 'js' 49 | }, 50 | 'moment': { main: 'moment.js', defaultExtension: 'js' }, 51 | 'ngx-bootstrap': { format: 'cjs', main: 'bundles/ngx-bootstrap.umd.js', defaultExtension: 'js' }, 52 | 'ng2-slim-loading-bar': { main: 'index.js', defaultExtension: 'js' }, 53 | 'symbol-observable': { main: 'index.js', defaultExtension: 'js' } 54 | } 55 | }); 56 | })(this); 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "skipLibCheck": true 12 | } 13 | } --------------------------------------------------------------------------------