From a52ee611fcbdbcdac2152a8d5305e57a26a98c02 Mon Sep 17 00:00:00 2001 From: Michal Dvorak Date: Thu, 15 Feb 2018 19:34:35 +0100 Subject: [PATCH] #12 Adapted first version of Angulars own Tour of Heroes sample, RESTful way --- package-lock.json | 73 ++++++++++------- package.json | 24 +++--- proxy.conf.js | 48 ------------ src/app/app-routing.module.ts | 29 +++++++ src/app/app.component.css | 34 ++++++++ src/app/app.component.html | 33 +++----- src/app/app.component.ts | 25 ++---- src/app/app.module.ts | 62 ++++++++------- src/app/components/json.component.ts | 43 ---------- src/app/components/sample.component.ts | 39 ---------- src/app/dashboard/dashboard.component.css | 78 +++++++++++++++++++ src/app/dashboard/dashboard.component.html | 12 +++ src/app/dashboard/dashboard.component.spec.ts | 25 ++++++ src/app/dashboard/dashboard.component.ts | 19 +++++ src/app/hero-detail/hero-detail.component.css | 35 +++++++++ .../hero-detail/hero-detail.component.html | 11 +++ src/app/hero-detail/hero-detail.component.ts | 31 ++++++++ src/app/hero.ts | 10 +++ src/app/heroes/heroes.component.css | 75 ++++++++++++++++++ src/app/heroes/heroes.component.html | 22 ++++++ src/app/heroes/heroes.component.spec.ts | 25 ++++++ src/app/heroes/heroes.component.ts | 40 ++++++++++ src/app/in-memory-data.service.ts | 76 ++++++++++++++++++ src/app/message-http-interceptor.ts | 27 +++++++ src/app/message.service.spec.ts | 15 ++++ src/app/message.service.ts | 14 ++++ src/app/messages/messages.component.css | 40 ++++++++++ src/app/messages/messages.component.html | 9 +++ src/app/messages/messages.component.spec.ts | 25 ++++++ src/app/messages/messages.component.ts | 17 ++++ src/app/styles.css | 76 +++++++++++++++++- src/index.html | 2 - 32 files changed, 849 insertions(+), 245 deletions(-) delete mode 100644 proxy.conf.js create mode 100644 src/app/app-routing.module.ts create mode 100644 src/app/app.component.css delete mode 100644 src/app/components/json.component.ts delete mode 100644 src/app/components/sample.component.ts create mode 100644 src/app/dashboard/dashboard.component.css create mode 100644 src/app/dashboard/dashboard.component.html create mode 100644 src/app/dashboard/dashboard.component.spec.ts create mode 100644 src/app/dashboard/dashboard.component.ts create mode 100644 src/app/hero-detail/hero-detail.component.css create mode 100644 src/app/hero-detail/hero-detail.component.html create mode 100644 src/app/hero-detail/hero-detail.component.ts create mode 100644 src/app/hero.ts create mode 100644 src/app/heroes/heroes.component.css create mode 100644 src/app/heroes/heroes.component.html create mode 100644 src/app/heroes/heroes.component.spec.ts create mode 100644 src/app/heroes/heroes.component.ts create mode 100644 src/app/in-memory-data.service.ts create mode 100644 src/app/message-http-interceptor.ts create mode 100644 src/app/message.service.spec.ts create mode 100644 src/app/message.service.ts create mode 100644 src/app/messages/messages.component.css create mode 100644 src/app/messages/messages.component.html create mode 100644 src/app/messages/messages.component.spec.ts create mode 100644 src/app/messages/messages.component.ts diff --git a/package-lock.json b/package-lock.json index cf51cfb..5536699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,9 +53,9 @@ } }, "@angular/animations": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.4.tgz", - "integrity": "sha512-kLOUORV/2GdYsNSwmUsB3eEL+nAoBZYKgibYLkVy6oecrIbdFMWiNzLcFjX/avcMnb1UNMk24Hd7Of4C2UawPA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.5.tgz", + "integrity": "sha512-70ElCmaeDxLQc2OkgYhJjXj4zjtdjI4K1D5ZZm/uSPLlUcqC6uf6skCXlhMawQoPbsL/SXE5xw2HlMgEbhUysw==", "requires": { "tslib": "1.8.0" } @@ -126,25 +126,25 @@ } }, "@angular/common": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.4.tgz", - "integrity": "sha512-PNtg7lzCBUgYo5Rj+/j11EVKhLfrUkkh81ecBwexk6VcDJebmvBO1HdGppV5UPzEH/StL1mTwLc95dOI0hHSJA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.5.tgz", + "integrity": "sha512-jagCxo+75pcTwjuO1ZheIiTlKBJ6REFKFWoUPTzaSS6fnzReFJ+VPf4Pb0bWtHL1lWvbvnzmITOJPB9wmuM3fg==", "requires": { "tslib": "1.8.0" } }, "@angular/compiler": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.4.tgz", - "integrity": "sha512-KFaGcm/5OKJRxXIxrS53IYPtqta9u2xLLedrWspxIvI59ImfzeZGnLGPhfrI0pbK7wY0rJ5YdGYQnzq33dh01A==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.5.tgz", + "integrity": "sha512-YU/r5omexkrrBF3bZaseWrc2Iotk6hIdUWkPIL3gPC0hKJ3wBeB3sHCBujPQXktWdMBbQRujNSMZtgra3Oh1xQ==", "requires": { "tslib": "1.8.0" } }, "@angular/compiler-cli": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.2.4.tgz", - "integrity": "sha512-nODdd7EuGzk1ME5UzpVa/lN1oKNypRt2oZoNYOkgNO2TQWD1jqOcozruit1eOEFHQhXO2JvPTzlt1dd6viHSCQ==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-5.2.5.tgz", + "integrity": "sha512-jRFMxUKpodzOBKdZc6OMse+CjK6xfTJssZQrYeIyqz2daobaIsMZP2hZX8s/PCfV8Vxa7XFwCJb7Fq2uyZKfHg==", "dev": true, "requires": { "chokidar": "1.7.0", @@ -154,47 +154,55 @@ } }, "@angular/core": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.4.tgz", - "integrity": "sha512-GPnxUf7g8Mz0AUttKKcqaw0m2xZujwwzojkg3xUIvHrNFFF5/HH5549PfnE1jD7qkmnDFx5j3IPuNkwYHW6XvA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.5.tgz", + "integrity": "sha512-Uo7R3LrsvA24JkRbwXWUZWp7NSEpwdTUxT1NScyjrBK+t8ybSL5/42Jo21md5M4pjeCsIgUXlGoCm1QtT5aYnQ==", "requires": { "tslib": "1.8.0" } }, "@angular/forms": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.4.tgz", - "integrity": "sha512-0k6rs2k85wcBq0WPAjxNbtBu1wq/1fUSFaBLbpnrwwHeCLJI5aAjG2/f3jv/17a/ek7/WZ3lxXtHzNMMdaD/Iw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.5.tgz", + "integrity": "sha512-3feqqTuv9rIu7ZOsLCtM/ugNFz5RPujLHkE8bU1gsMM4/eMYruIFir2vbjnhMkD3K6KptEg4iO6tDW18diwXug==", + "requires": { + "tslib": "1.8.0" + } + }, + "@angular/http": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.5.tgz", + "integrity": "sha512-VqTCkAnebe+M9Bqrfp1QYpBQCTbXide/NxrQfwiJY87kjKFeRBuy9/XH/2S5wIwlF5Yx3bmlaIufd9VI5r/0aQ==", "requires": { "tslib": "1.8.0" } }, "@angular/language-service": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-5.2.4.tgz", - "integrity": "sha512-slze+UcBzm+p/pAIGxah3errOlWsUVJ5I4RNgErmR3rlylt5YNWNM/LClbDVGk8zoW+NBg0fwIxUbqK6qnk5Cw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-5.2.5.tgz", + "integrity": "sha512-UWNbECu8svXmrgbTL03Fr+Dn06aPCZZLScmCOGVT5lkdsiJPAJpWAvKVM2Y0nzH0PmvekHw7INtV5lwfJOijYQ==", "dev": true }, "@angular/platform-browser": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.4.tgz", - "integrity": "sha512-chv6h2aHQ/QoVA4Y6rpPpSju7vyLg/iMh516GxpGYVk6bHEdrH9pHJPulPcrt/LTd7lMAAHE3YmvYWVU6aDsaQ==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.5.tgz", + "integrity": "sha512-iPAuoG/c3pD3hnk1g0VgJu/pzNITvLQyT0W71MDMSuxLxs291kq+U2jklm40pStISd1mPbCNKmvz/7M+WbdLhg==", "requires": { "tslib": "1.8.0" } }, "@angular/platform-browser-dynamic": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.4.tgz", - "integrity": "sha512-B3pv6FUTWA1daDYhx6b77FCFCzHQPuCyrsJQwMSSu6Xt+CYn2gc3dS0ph3B6cV6mnt1qIbEpML+Vp5Bi9x0Mkw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.5.tgz", + "integrity": "sha512-IMEe2qUTC3CA3KoswmJJs+O2Lkyd5GXgl5ULupqhhm/TOL2FLk00kwv8k3Epaf2d1wXcjK3BMG7aAwc6RLH7QA==", "requires": { "tslib": "1.8.0" } }, "@angular/router": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.4.tgz", - "integrity": "sha512-sg3iCThhbfv/6zARdKbHNLc7Xe1Rt1deit55b3K+WlrHX7GhsuJPLcitrNaADIcgDKbNT9XrwBaNirAEip9hxA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.5.tgz", + "integrity": "sha512-I8U0iy59lz0dAxU4zxRQHagfUPWF+MikLNMirRL1lrA49PG+5K1tiuIQ6p+8fZFAJ5UXwNHyXqYuWqsKRiVBHQ==", "dev": true, "requires": { "tslib": "1.8.0" @@ -605,6 +613,11 @@ } } }, + "angular-in-memory-web-api": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/angular-in-memory-web-api/-/angular-in-memory-web-api-0.5.3.tgz", + "integrity": "sha512-1QPwwXG8R/2s7EbHh13HDiJYsk4sdBHNxHJHZHJ/Kxb4T9OG+bb1kGcXzY9UrJkEVxOtUW0ozvL4p/HmeIEszg==" + }, "ansi-gray": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", diff --git a/package.json b/package.json index 6c82c29..87ea14b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "bootstrap": "npm link dist/angular-resource-router && npm link angular-resource-router", "postbootstrap": "rimraf dist/", "ng": "ng", - "start": "ng serve --proxy-config=proxy.conf.js", + "start": "ng serve", "prebuild": "ng lint && ng test --watch=false", "build": "ng-packagr -p src/lib/ng-package.json && cpr README.md dist/angular-resource-router/README.md && cpr LICENSE dist/angular-resource-router/LICENSE", "buildapp": "ng build --target=production --env=prod --sourcemaps", @@ -21,22 +21,24 @@ "ng-packagr": "ng-packagr" }, "dependencies": { - "@angular/animations": "^5.1.2", - "@angular/common": "^5.1.2", - "@angular/compiler": "^5.1.2", - "@angular/core": "^5.1.2", - "@angular/forms": "^5.1.2", - "@angular/platform-browser": "^5.1.2", - "@angular/platform-browser-dynamic": "^5.1.2", + "@angular/animations": "^5.2.5", + "@angular/common": "^5.2.5", + "@angular/compiler": "^5.2.5", + "@angular/core": "^5.2.5", + "@angular/forms": "^5.2.5", + "@angular/http": "^5.2.5", + "@angular/platform-browser": "^5.2.5", + "@angular/platform-browser-dynamic": "^5.2.5", + "angular-in-memory-web-api": "~0.5.1", "core-js": "^2.4.1", "rxjs": "^5.5.6", "zone.js": "^0.8.20" }, "devDependencies": { "@angular/cli": "1.6.8", - "@angular/compiler-cli": "^5.1.2", - "@angular/language-service": "^5.1.2", - "@angular/router": "^5.1.2", + "@angular/compiler-cli": "^5.2.5", + "@angular/language-service": "^5.2.5", + "@angular/router": "^5.2.5", "@compodoc/compodoc": "1.0.5", "@types/jasmine": "2.8.6", "@types/jasminewd2": "2.0.3", diff --git a/proxy.conf.js b/proxy.conf.js deleted file mode 100644 index aa5c3b4..0000000 --- a/proxy.conf.js +++ /dev/null @@ -1,48 +0,0 @@ -const TARGET = "http://private-anon-39155185b1-resourcerouterexample.apiary-mock.com"; - -function replaceHost(data, req) { - const hostRegExp = new RegExp(TARGET.replace(/([^a-zA-Z0-9])/g, '\\$1') + '/', 'g'); - return data.toString().replace(hostRegExp, 'http://' + req.hostname + ':4200/') -} - -const PROXY_CONFIG = { - "/api": { - "target": TARGET, - "secure": false, - "cookieDomainRewrite": true, - "changeOrigin": true, - "logLevel": "debug", - "onProxyReq": function (proxyReq, req, res) { - proxyReq.setHeader('Host', TARGET.replace(/^\w+:\/\//, '')); - }, - "onProxyRes": function (proxyRes, req, res) { - // Handle redirect - if (proxyRes.headers['location']) { - proxyRes.headers['location'] = replaceHost(proxyRes.headers['location'], req); - } - - // Remove Content-Length - we change it, and browser can do without it - delete proxyRes.headers['content-length']; - - // Content replacement logic - const _end = res.end; - let response = ''; - - // Collect response - proxyRes.on('data', function (data) { - response += data.toString(); - }); - - // Defer all writes - res.write = function () { - }; - - // Write final buffer - res.end = function () { - _end.call(res, replaceHost(response, req)); - } - } - } -}; - -module.exports = PROXY_CONFIG; diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts new file mode 100644 index 0000000..887f2b6 --- /dev/null +++ b/src/app/app-routing.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { ResourceRouterModule } from 'angular-resource-router'; +import { DashboardComponent } from './dashboard/dashboard.component'; +import { HeroesComponent } from './heroes/heroes.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; + +@NgModule({ + imports: [ + ResourceRouterModule.forTypes([ + { + type: 'application/x.dashboard', + component: DashboardComponent + }, + { + type: 'application/x.heroes', + component: HeroesComponent + }, + { + type: 'application/x.hero', + component: HeroDetailComponent + } + ]) + ], + exports: [ + ResourceRouterModule + ], +}) +export class AppRoutingModule { +} diff --git a/src/app/app.component.css b/src/app/app.component.css new file mode 100644 index 0000000..f103a1c --- /dev/null +++ b/src/app/app.component.css @@ -0,0 +1,34 @@ +/* AppComponent's private CSS styles */ +h1 { + font-size: 1.2em; + color: #999; + margin-bottom: 0; +} + +h2 { + font-size: 2em; + margin-top: 0; + padding-top: 0; +} + +nav a { + padding: 5px 10px; + text-decoration: none; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} + +nav a:visited, a:link { + color: #607D8B; +} + +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} + +nav a.active { + color: #039be5; +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 7d6a99e..9de8b7c 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,24 +1,11 @@ -

See API definition

- - -
- +A - -A -
- -

ROOT

-

{{resource.url}}

-

{{resource.loading ? 'Loading...' : 'Loaded!'}}

- +

{{title}}

+ + + + - - - - - - - - - - - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index a472dc8..757ae95 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,28 +1,15 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ApiLocation, bindUrl, ResourceData } from '../lib/src'; -import { Subscription } from 'rxjs/Subscription'; - -// TODO asi by to chtelo prejmenovat ResourceData, protoze to nepopisuje, co trida dela - drzi location a aktualni data, -// TODO tedy takovy container - pak se da prejmenovat i ViewData.target +import { Component } from '@angular/core'; +import { ApiLocation } from '../lib/src'; @Component({ selector: 'app-root', templateUrl: './app.component.html', - providers: [ResourceData] + styleUrls: ['./app.component.css'] }) -export class AppComponent implements OnInit, OnDestroy { - - private urlSubscription = Subscription.EMPTY; +export class AppComponent { - constructor(public apiLocation: ApiLocation, - public resource: ResourceData) { - } - - ngOnInit() { - this.urlSubscription = bindUrl(this.apiLocation, this.resource); - } + title = 'Tour of Heroes'; - ngOnDestroy() { - this.urlSubscription.unsubscribe(); + constructor(public apiLocation: ApiLocation) { } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0d564a8..790a08e 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -2,49 +2,53 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; -import { JsonComponent } from './components/json.component'; -import { SampleComponent } from './components/sample.component'; -import { HttpClientModule } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { environment } from './environments/environment'; import { ResourceRouterModule } from 'angular-resource-router'; +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; +import { AppRoutingModule } from './app-routing.module'; +import { MessageService } from './message.service'; +import { DashboardComponent } from './dashboard/dashboard.component'; +import { HeroesComponent } from './heroes/heroes.component'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; +import { MessagesComponent } from './messages/messages.component'; +import { MessageHttpInterceptor } from './message-http-interceptor'; @NgModule({ - declarations: [ - AppComponent, - JsonComponent, - SampleComponent - ], imports: [ HttpClientModule, BrowserModule, FormsModule, + // Mock HTTP backend + HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { + apiBase: environment.prefix, + dataEncapsulation: false + }), + // Router config ResourceRouterModule.configure({ prefix: environment.prefix, useHash: environment.useHash }), - ResourceRouterModule.forTypes([ - { - type: 'application/x.questions', - component: JsonComponent - }, - { - type: 'application/x.question', - component: JsonComponent - }, - { - type: 'application/x.sample', - component: SampleComponent - } - ]) + // Routes + AppRoutingModule ], - exports: [ - BrowserModule, - FormsModule, - HttpClientModule + declarations: [ + AppComponent, + MessagesComponent, + DashboardComponent, + HeroesComponent, + HeroDetailComponent, + ], + providers: [ + MessageService, + { + provide: HTTP_INTERCEPTORS, + useClass: MessageHttpInterceptor, + multi: true + }, ], - bootstrap: [ - AppComponent - ] + bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/components/json.component.ts b/src/app/components/json.component.ts deleted file mode 100644 index cfca33f..0000000 --- a/src/app/components/json.component.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedView } from 'angular-resource-router'; - -@Component({ - template: `

JSON {{viewData.type}}

-
{{viewData.body | json}}
- - {{link.title}} (_self) - - - -
- /246 (default) - /.. (default) -
- -
- /246 (_self) - /.. (_self) -
- -
-

NESTED {{nested.target.id}}

- -
- ` -}) -export class JsonComponent implements OnInit { - - viewData: any; - - constructor(public view: ActivatedView) { - } - - ngOnInit(): void { - this.view.data.subscribe(data => this.viewData = data); - } -} diff --git a/src/app/components/sample.component.ts b/src/app/components/sample.component.ts deleted file mode 100644 index f9d25eb..0000000 --- a/src/app/components/sample.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedView } from 'angular-resource-router'; - -export interface SampleData { - - _links?: any; - - name: string; - address?: string; - age?: number; -} - - -@Component({ - template: `

SAmple

-
- - - - - - - - -
- - {{link.title}}` -}) -export class SampleComponent implements OnInit { - - data: SampleData; - - constructor(public view: ActivatedView) { - } - - ngOnInit(): void { - this.view.data.subscribe(data => this.data = data.body); - } -} diff --git a/src/app/dashboard/dashboard.component.css b/src/app/dashboard/dashboard.component.css new file mode 100644 index 0000000..56a8d42 --- /dev/null +++ b/src/app/dashboard/dashboard.component.css @@ -0,0 +1,78 @@ +/* DashboardComponent's private CSS styles */ +[class*='col-'] { + float: left; + padding-right: 20px; + padding-bottom: 20px; +} + +[class*='col-']:last-of-type { + padding-right: 0; +} + +a { + text-decoration: none; +} + +*, *:after, *:before { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +h3 { + text-align: center; + margin-bottom: 0; +} + +h4 { + position: relative; +} + +.grid { + margin: 0; +} + +.col-1-4 { + width: 25%; +} + +.module { + padding: 20px; + text-align: center; + color: #eee; + max-height: 120px; + min-width: 120px; + background-color: #607D8B; + border-radius: 2px; +} + +.module:hover { + background-color: #EEE; + cursor: pointer; + color: #607d8b; +} + +.grid-pad { + padding: 10px 0; +} + +.grid-pad > [class*='col-']:last-of-type { + padding-right: 20px; +} + +@media (max-width: 600px) { + .module { + font-size: 10px; + max-height: 75px; + } +} + +@media (max-width: 1024px) { + .grid { + margin: 0; + } + + .module { + min-width: 60px; + } +} diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html new file mode 100644 index 0000000..ae73da7 --- /dev/null +++ b/src/app/dashboard/dashboard.component.html @@ -0,0 +1,12 @@ +

Top Heroes

+ + + + diff --git a/src/app/dashboard/dashboard.component.spec.ts b/src/app/dashboard/dashboard.component.spec.ts new file mode 100644 index 0000000..767b969 --- /dev/null +++ b/src/app/dashboard/dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: DashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [DashboardComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts new file mode 100644 index 0000000..5d55d23 --- /dev/null +++ b/src/app/dashboard/dashboard.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit } from '@angular/core'; +import { Hero } from '../hero'; +import { ActivatedView } from 'angular-resource-router'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.css'] +}) +export class DashboardComponent implements OnInit { + heroes: Hero[] = []; + + constructor(private readonly view: ActivatedView) { + } + + ngOnInit() { + this.view.body.subscribe(heroes => this.heroes = heroes); + } +} diff --git a/src/app/hero-detail/hero-detail.component.css b/src/app/hero-detail/hero-detail.component.css new file mode 100644 index 0000000..616751b --- /dev/null +++ b/src/app/hero-detail/hero-detail.component.css @@ -0,0 +1,35 @@ +/* HeroDetailComponent's private CSS styles */ +label { + display: inline-block; + width: 3em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} + +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} + +button { + margin-top: 20px; + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; +} + +button:hover { + background-color: #cfd8dc; +} + +button:disabled { + background-color: #eee; + color: #ccc; + cursor: auto; +} diff --git a/src/app/hero-detail/hero-detail.component.html b/src/app/hero-detail/hero-detail.component.html new file mode 100644 index 0000000..6fa498e --- /dev/null +++ b/src/app/hero-detail/hero-detail.component.html @@ -0,0 +1,11 @@ +
+

{{ hero.name | uppercase }} Details

+
id: {{hero.id}}
+
+ +
+ + +
diff --git a/src/app/hero-detail/hero-detail.component.ts b/src/app/hero-detail/hero-detail.component.ts new file mode 100644 index 0000000..a79c21a --- /dev/null +++ b/src/app/hero-detail/hero-detail.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { Location } from '@angular/common'; +import { Hero } from '../hero'; +import { ActivatedView } from 'angular-resource-router'; +import { HttpClient } from '@angular/common/http'; + +@Component({ + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.css'] +}) +export class HeroDetailComponent implements OnInit { + hero: Hero; + + constructor(private readonly view: ActivatedView, + private readonly http: HttpClient, + private location: Location) { + } + + ngOnInit(): void { + this.view.body.subscribe(hero => this.hero = hero); + } + + goBack(): void { + this.location.back(); + } + + save(): void { + const url = this.hero._links.self.href; + this.http.post(url, this.hero).subscribe(() => this.goBack()); + } +} diff --git a/src/app/hero.ts b/src/app/hero.ts new file mode 100644 index 0000000..d0e10c0 --- /dev/null +++ b/src/app/hero.ts @@ -0,0 +1,10 @@ +import { Link } from 'angular-resource-router'; + +export interface Hero { + id: number; + name: string; + + _links: { + self: Link; + }; +} diff --git a/src/app/heroes/heroes.component.css b/src/app/heroes/heroes.component.css new file mode 100644 index 0000000..53d7852 --- /dev/null +++ b/src/app/heroes/heroes.component.css @@ -0,0 +1,75 @@ +/* HeroesComponent's private CSS styles */ +.heroes { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; +} + +.heroes li { + position: relative; + cursor: pointer; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 1.6em; + border-radius: 4px; +} + +.heroes li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} + +.heroes a { + color: #888; + text-decoration: none; + position: relative; + display: block; + width: 250px; +} + +.heroes a:hover { + color: #607D8B; +} + +.heroes .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #607D8B; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + height: 1.8em; + min-width: 16px; + text-align: right; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} + +.button { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; + font-family: Arial; +} + +button:hover { + background-color: #cfd8dc; +} + +button.delete { + position: relative; + left: 194px; + top: -32px; + background-color: gray !important; + color: white; +} + diff --git a/src/app/heroes/heroes.component.html b/src/app/heroes/heroes.component.html new file mode 100644 index 0000000..4c7311f --- /dev/null +++ b/src/app/heroes/heroes.component.html @@ -0,0 +1,22 @@ +

My Heroes

+ +
+ + + +
+ + diff --git a/src/app/heroes/heroes.component.spec.ts b/src/app/heroes/heroes.component.spec.ts new file mode 100644 index 0000000..4729423 --- /dev/null +++ b/src/app/heroes/heroes.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroesComponent } from './heroes.component'; + +describe('HeroesComponent', () => { + let component: HeroesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [HeroesComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HeroesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/heroes/heroes.component.ts b/src/app/heroes/heroes.component.ts new file mode 100644 index 0000000..492449c --- /dev/null +++ b/src/app/heroes/heroes.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { Hero } from '../hero'; +import { ActivatedView } from 'angular-resource-router'; + +@Component({ + selector: 'app-heroes', + templateUrl: './heroes.component.html', + styleUrls: ['./heroes.component.css'] +}) +export class HeroesComponent implements OnInit { + heroes: Hero[]; + + constructor(private readonly view: ActivatedView) { + } + + ngOnInit() { + this.view.body.subscribe(heroes => this.heroes = heroes); + } + + add(name: string): void { + name = name.trim(); + if (!name) { + return; + } + /* TODO + this.heroService.addHero({name} as Hero) + .subscribe(hero => { + this.heroes.push(hero); + }); + */ + } + + delete(hero: Hero): void { + /* TODO + this.heroes = this.heroes.filter(h => h !== hero); + this.heroService.deleteHero(hero).subscribe(); + */ + } + +} diff --git a/src/app/in-memory-data.service.ts b/src/app/in-memory-data.service.ts new file mode 100644 index 0000000..65d789b --- /dev/null +++ b/src/app/in-memory-data.service.ts @@ -0,0 +1,76 @@ +import { InMemoryDbService, RequestInfo } from 'angular-in-memory-web-api'; +import { Observable } from 'rxjs/Observable'; +import { HttpHeaders } from '@angular/common/http'; +import { Hero } from './hero'; + +const DASHBOARD_TYPE = 'application/x.dashboard'; +const HEROES_TYPE = 'application/x.heroes'; +const HERO_TYPE = 'application/x.hero'; + +export class InMemoryDataService implements InMemoryDbService { + createDb() { + const heroes = [ + createHero({id: 11, name: 'Mr. Nice'}), + createHero({id: 12, name: 'Narco'}), + createHero({id: 13, name: 'Bombasto'}), + createHero({id: 14, name: 'Celeritas'}), + createHero({id: 15, name: 'Magneta'}), + createHero({id: 16, name: 'RubberMan'}), + createHero({id: 17, name: 'Dynama'}), + createHero({id: 18, name: 'Dr IQ'}), + createHero({id: 19, name: 'Magma'}), + createHero({id: 20, name: 'Tornado'}) + ]; + return {heroes}; + } + + get(request: RequestInfo): Observable | undefined { + if (request.collectionName === 'heroes' && request.id) { + const data = request.utils.findById(request.collection, request.id); + + if (data) { + return request.utils.createResponse$(() => ({ + url: request.url, + body: JSON.stringify(data), + status: 200, + statusText: 'OK', + headers: new HttpHeaders({ + 'Content-Type': HERO_TYPE + }) + })); + } + } else if (request.collectionName === 'heroes') { + return request.utils.createResponse$(() => ({ + url: request.url, + body: JSON.stringify(request.collection), + status: 200, + statusText: 'OK', + headers: new HttpHeaders({ + 'Content-Type': HEROES_TYPE + }) + })); + } else if (request.url.endsWith('/api/dashboard')) { + const collection: Hero[] = (request.utils.getDb() as any).heroes; + + return request.utils.createResponse$(() => ({ + url: request.url, + body: JSON.stringify(collection.slice(1, 5)), + status: 200, + statusText: 'OK', + headers: new HttpHeaders({ + 'Content-Type': DASHBOARD_TYPE + }) + })); + } + } +} + +function createHero(obj: { id: number, name: string }) { + return { + id: obj.id, + name: obj.name, + _links: { + self: {href: '/api/heroes/' + obj.id} + } + }; +} diff --git a/src/app/message-http-interceptor.ts b/src/app/message-http-interceptor.ts new file mode 100644 index 0000000..7504ac3 --- /dev/null +++ b/src/app/message-http-interceptor.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest, HttpResponseBase } from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/do'; +import { MessageService } from './message.service'; + +/** + * Diagnostic interceptor to visualise requests to mocked API. + */ +@Injectable() +export class MessageHttpInterceptor implements HttpInterceptor { + + constructor(private readonly messageService: MessageService) { + } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).do((res: HttpEvent) => { + if (res instanceof HttpResponseBase) { + this.messageService.add(`${req.method} ${req.url} => ${res.status} ${res.statusText} (${dumpHeaders(res.headers)})`); + } + }); + } +} + +function dumpHeaders(headers: HttpHeaders) { + return headers.keys().map(h => h + ': ' + (headers.getAll(h) || []).join(',')).join(', '); +} diff --git a/src/app/message.service.spec.ts b/src/app/message.service.spec.ts new file mode 100644 index 0000000..e1004a0 --- /dev/null +++ b/src/app/message.service.spec.ts @@ -0,0 +1,15 @@ +import { inject, TestBed } from '@angular/core/testing'; + +import { MessageService } from './message.service'; + +describe('MessageService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MessageService] + }); + }); + + it('should be created', inject([MessageService], (service: MessageService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/message.service.ts b/src/app/message.service.ts new file mode 100644 index 0000000..1c56a49 --- /dev/null +++ b/src/app/message.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class MessageService { + messages: string[] = []; + + add(message: string) { + this.messages.push(message); + } + + clear() { + this.messages = []; + } +} diff --git a/src/app/messages/messages.component.css b/src/app/messages/messages.component.css new file mode 100644 index 0000000..d8bdab9 --- /dev/null +++ b/src/app/messages/messages.component.css @@ -0,0 +1,40 @@ +/* MessagesComponent's private CSS styles */ +h2 { + color: red; + font-family: Arial, Helvetica, sans-serif; + font-weight: lighter; +} + +body { + margin: 2em; +} + +body, input[text], button { + color: crimson; + font-family: Cambria, Georgia, sans-serif; +} + +button.clear { + font-family: Arial, sans-serif; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; +} + +button:hover { + background-color: #cfd8dc; +} + +button:disabled { + background-color: #eee; + color: #aaa; + cursor: auto; +} + +button.clear { + color: #888; + margin-bottom: 12px; +} diff --git a/src/app/messages/messages.component.html b/src/app/messages/messages.component.html new file mode 100644 index 0000000..f6b6b86 --- /dev/null +++ b/src/app/messages/messages.component.html @@ -0,0 +1,9 @@ +
+ +

Messages

+ +
{{message}}
+ +
diff --git a/src/app/messages/messages.component.spec.ts b/src/app/messages/messages.component.spec.ts new file mode 100644 index 0000000..209b7d8 --- /dev/null +++ b/src/app/messages/messages.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MessagesComponent } from './messages.component'; + +describe('MessagesComponent', () => { + let component: MessagesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MessagesComponent] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MessagesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/messages/messages.component.ts b/src/app/messages/messages.component.ts new file mode 100644 index 0000000..d8fa037 --- /dev/null +++ b/src/app/messages/messages.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; +import { MessageService } from '../message.service'; + +@Component({ + selector: 'app-messages', + templateUrl: './messages.component.html', + styleUrls: ['./messages.component.css'] +}) +export class MessagesComponent implements OnInit { + + constructor(public messageService: MessageService) { + } + + ngOnInit() { + } + +} diff --git a/src/app/styles.css b/src/app/styles.css index 90d4ee0..a1149fb 100644 --- a/src/app/styles.css +++ b/src/app/styles.css @@ -1 +1,75 @@ -/* You can add global styles to this file, and also import other style files */ +/* Master Styles */ +h1 { + color: #369; + font-family: Arial, Helvetica, sans-serif; + font-size: 250%; +} + +h2, h3 { + color: #444; + font-family: Arial, Helvetica, sans-serif; + font-weight: lighter; +} + +body { + margin: 2em; +} + +body, input[text], button { + color: #888; + font-family: Cambria, Georgia; +} + +a { + cursor: pointer; + cursor: hand; +} + +button { + font-family: Arial; + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + cursor: pointer; + cursor: hand; +} + +button:hover { + background-color: #cfd8dc; +} + +button:disabled { + background-color: #eee; + color: #aaa; + cursor: auto; +} + +/* Navigation link styles */ +nav a { + padding: 5px 10px; + text-decoration: none; + margin-right: 10px; + margin-top: 10px; + display: inline-block; + background-color: #eee; + border-radius: 4px; +} + +nav a:visited, a:link { + color: #607D8B; +} + +nav a:hover { + color: #039be5; + background-color: #CFD8DC; +} + +nav a.active { + color: #039be5; +} + +/* everywhere else */ +* { + font-family: Arial, Helvetica, sans-serif; +} diff --git a/src/index.html b/src/index.html index af61ab8..ced498f 100644 --- a/src/index.html +++ b/src/index.html @@ -7,8 +7,6 @@ -