├── src
├── assets
│ ├── .gitkeep
│ └── img
│ │ ├── examples
│ │ ├── shiba1.jpg
│ │ ├── shiba2.jpg
│ │ └── thumbup-icon.svg
│ │ ├── favicons
│ │ ├── favicon.ico
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── mstile-150x150.png
│ │ ├── apple-touch-icon.png
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── browserconfig.xml
│ │ ├── manifest.json
│ │ └── safari-pinned-tab.svg
│ │ ├── components
│ │ ├── grid-list.svg
│ │ ├── slide-toggle.svg
│ │ ├── button.svg
│ │ ├── snack-bar.svg
│ │ ├── card.svg
│ │ ├── menu.svg
│ │ ├── chips.svg
│ │ ├── sidenav.svg
│ │ ├── toolbar.svg
│ │ ├── dialog.svg
│ │ ├── progress-spinner.svg
│ │ ├── textarea.svg
│ │ ├── tooltip.svg
│ │ ├── button-toggle.svg
│ │ ├── input.svg
│ │ ├── tabs.svg
│ │ ├── progress-bar.svg
│ │ ├── list.svg
│ │ ├── select.svg
│ │ ├── autocomplete.svg
│ │ ├── radio.svg
│ │ ├── checkbox.svg
│ │ ├── slider.svg
│ │ ├── icon.svg
│ │ └── datepicker.svg
│ │ ├── component-categories
│ │ ├── nav.svg
│ │ ├── layout.svg
│ │ ├── buttons.svg
│ │ ├── modals.svg
│ │ └── forms.svg
│ │ ├── theme-demo-icon.svg
│ │ ├── homepage
│ │ ├── github-circle-white-transparent.svg
│ │ ├── angular-white-transparent.svg
│ │ ├── fastandconsistent.svg
│ │ ├── versatile.svg
│ │ ├── sprintzerotoapp.svg
│ │ ├── optimized.svg
│ │ └── material_splash.svg
│ │ └── angular-material-logo.svg
├── app
│ ├── app.constants.ts
│ ├── heroes
│ │ ├── models
│ │ │ ├── index.ts
│ │ │ └── hero.ts
│ │ ├── hero-form
│ │ │ ├── hero-form.component.scss
│ │ │ ├── hero-form.component.ts
│ │ │ └── hero-form.component.html
│ │ ├── index.ts
│ │ ├── heroes-list
│ │ │ ├── heroes-list.component.scss
│ │ │ ├── heroes-list.component.html
│ │ │ └── heroes-list.component.ts
│ │ ├── hero-detail
│ │ │ ├── hero-detail.component.html
│ │ │ ├── hero-detail.component.ts
│ │ │ └── hero-detail.component.scss
│ │ ├── heroes-routing.module.ts
│ │ └── heroes.module.ts
│ ├── root-store
│ │ ├── index.ts
│ │ ├── hero-form-store
│ │ │ ├── index.ts
│ │ │ ├── hero-form-store.module.ts
│ │ │ └── hero-form.reducers.ts
│ │ ├── hero-store
│ │ │ ├── index.ts
│ │ │ ├── hero-store.module.ts
│ │ │ ├── hero.selectors.ts
│ │ │ ├── hero.actions.ts
│ │ │ ├── hero.reducers.ts
│ │ │ └── hero.effects.ts
│ │ └── root-store.module.ts
│ ├── shared
│ │ ├── material
│ │ │ ├── index.ts
│ │ │ ├── error-state-matcher.ts
│ │ │ ├── mat-list-option-fix.ts
│ │ │ ├── material.module.ts
│ │ │ └── mat-select-view-adapter.ts
│ │ ├── theme-picker
│ │ │ ├── index.ts
│ │ │ ├── theme-picker.component.html
│ │ │ ├── theme-picker.module.ts
│ │ │ ├── theme-picker.component.spec.ts
│ │ │ ├── theme-picker.component.scss
│ │ │ └── theme-picker.component.ts
│ │ ├── navbar
│ │ │ ├── index.ts
│ │ │ ├── nav-bar.component.ts
│ │ │ ├── _navbar-theme.scss
│ │ │ ├── nav-bar.component.html
│ │ │ ├── nav-bar.component.spec.ts
│ │ │ ├── nav-bar.component.scss
│ │ │ └── nav-bar.module.ts
│ │ ├── content-header
│ │ │ ├── content-header.component.html
│ │ │ ├── content-header.component.ts
│ │ │ ├── content-header.module.ts
│ │ │ ├── content-header.component.scss
│ │ │ └── content-header.component.spec.ts
│ │ └── footer
│ │ │ ├── footer.module.ts
│ │ │ ├── footer.component.ts
│ │ │ ├── _footer-theme.scss
│ │ │ ├── footer.component.html
│ │ │ ├── footer.component.spec.ts
│ │ │ └── footer.component.scss
│ ├── movies
│ │ ├── models
│ │ │ └── movie.ts
│ │ ├── movie-detail
│ │ │ ├── movie-detail.component.html
│ │ │ ├── movie-detail.component.css
│ │ │ └── movie-detail.component.ts
│ │ ├── movies.module.ts
│ │ ├── movies-routing.module.ts
│ │ ├── movies-list
│ │ │ ├── movies-list.component.html
│ │ │ ├── movies-list.component.ts
│ │ │ └── movies-list.component.css
│ │ └── movies.store.ts
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.component.scss
│ ├── app-routing.module.ts
│ ├── app.component.spec.ts
│ ├── services
│ │ ├── heroes.service.ts
│ │ ├── movies.service.ts
│ │ ├── theme.service.ts
│ │ ├── style.service.ts
│ │ ├── theme.service.spec.ts
│ │ └── style.service.spec.ts
│ ├── resolvers
│ │ ├── heroes.resolver.ts
│ │ └── hero.resolver.ts
│ └── app.module.ts
├── favicon.ico
├── environments
│ ├── environment.dev.ts
│ ├── environment.prod.ts
│ └── environment.ts
├── styles.scss
├── styles
│ ├── custom-themes
│ │ ├── indigo-pink.scss
│ │ ├── deeppurple-amber.scss
│ │ ├── pink-bluegrey.scss
│ │ └── purple-green.scss
│ ├── _navbar-theme.scss
│ ├── _component-sidenav-theme.scss
│ ├── _general.scss
│ └── _constants.scss
├── main.ts
├── index.html
├── test.ts
├── _app-theme.scss
├── highlightjs
│ ├── material-light.scss
│ └── material-dark.scss
└── polyfills.ts
├── .editorconfig
├── e2e
├── tsconfig.json
├── src
│ ├── app.po.ts
│ └── app.e2e-spec.ts
└── protractor.conf.js
├── tsconfig.app.json
├── tsconfig.spec.json
├── browserslist
├── tsconfig.json
├── .gitignore
├── README.md
├── karma.conf.js
├── package.json
├── tslint.json
└── angular.json
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/app.constants.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/heroes/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hero';
2 |
--------------------------------------------------------------------------------
/src/app/root-store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './root-store.module';
2 |
--------------------------------------------------------------------------------
/src/app/shared/material/index.ts:
--------------------------------------------------------------------------------
1 | export * from './material.module';
2 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/index.ts:
--------------------------------------------------------------------------------
1 | export * from './theme-picker.component';
2 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/favicon.ico
--------------------------------------------------------------------------------
/src/app/heroes/hero-form/hero-form.component.scss:
--------------------------------------------------------------------------------
1 | .hero-form {
2 | width: 250px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/movies/models/movie.ts:
--------------------------------------------------------------------------------
1 | export class Movie {
2 | id?: string;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/index.ts:
--------------------------------------------------------------------------------
1 | export * from './nav-bar.component';
2 | export * from './nav-bar.module';
3 |
--------------------------------------------------------------------------------
/src/assets/img/examples/shiba1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/examples/shiba1.jpg
--------------------------------------------------------------------------------
/src/assets/img/examples/shiba2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/examples/shiba2.jpg
--------------------------------------------------------------------------------
/src/assets/img/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/favicon.ico
--------------------------------------------------------------------------------
/src/app/root-store/hero-form-store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hero-form.reducers';
2 | export * from './hero-form-store.module';
3 |
--------------------------------------------------------------------------------
/src/assets/img/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/src/assets/img/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/img/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/assets/img/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/assets/img/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/assets/img/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/2018/tour-of-heroes/master/src/assets/img/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/app/shared/content-header/content-header.component.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/app/heroes/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hero-detail/hero-detail.component';
2 | export * from './heroes-list/heroes-list.component';
3 | export * from './heroes.module';
4 |
--------------------------------------------------------------------------------
/src/environments/environment.dev.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false,
3 | appApi: {
4 | baseUrl: 'http://localhost:3000'
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | appApi: {
4 | baseUrl: 'http://localhost:8080'
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hero.actions';
2 | export * from './hero.effects';
3 | export * from './hero.reducers';
4 | export * from './hero.selectors';
5 | export * from './hero-store.module';
6 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import './app-theme';
2 | @import './styles/general';
3 |
4 | // Include material core styles.
5 | @include mat-core();
6 |
7 | @include angular-material-theme($theme);
8 | @include material-docs-app-theme($theme);
9 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | max_line_length = off
12 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styleUrls: ['./app.component.scss']
7 | })
8 | export class AppComponent {
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/heroes/models/hero.ts:
--------------------------------------------------------------------------------
1 | export class Hero {
2 | id?: string;
3 | name: string;
4 | // tslint:disable-next-line:variable-name
5 | full_name: string;
6 | thumb: string;
7 | photo: string;
8 | description: string;
9 | text: string;
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/img/components/grid-list.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/shared/footer/footer.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {FooterComponent} from './footer.component';
3 |
4 | @NgModule({
5 | exports: [FooterComponent],
6 | declarations: [FooterComponent],
7 | })
8 | export class FooterModule {
9 | }
10 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../out-tsc/e2e",
5 | "module": "commonjs",
6 | "target": "es5",
7 | "types": [
8 | "jasmine",
9 | "jasminewd2",
10 | "node"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/shared/footer/footer.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-footer',
5 | templateUrl: './footer.component.html',
6 | styleUrls: ['./footer.component.scss']
7 | })
8 | export class FooterComponent {
9 | }
10 |
11 |
12 |
--------------------------------------------------------------------------------
/e2e/src/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, by, element } from 'protractor';
2 |
3 | export class AppPage {
4 | navigateTo() {
5 | return browser.get(browser.baseUrl) as Promise;
6 | }
7 |
8 | getTitleText() {
9 | return element(by.css('app-root h1')).getText() as Promise;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/img/component-categories/nav.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #3f51b5
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/styles/custom-themes/indigo-pink.scss:
--------------------------------------------------------------------------------
1 | @import 'app-theme';
2 |
3 | // Define the light theme.
4 | $primary: mat-palette($mat-indigo);
5 | $accent: mat-palette($mat-pink, A200, A100, A400);
6 |
7 | $theme: mat-light-theme($primary, $accent);
8 | @include angular-material-theme($theme);
9 | @include material-docs-app-theme($theme);
10 |
--------------------------------------------------------------------------------
/src/styles/custom-themes/deeppurple-amber.scss:
--------------------------------------------------------------------------------
1 | @import 'app-theme';
2 |
3 | // Define the light theme.
4 | $primary: mat-palette($mat-deep-purple);
5 | $accent: mat-palette($mat-amber, A200, A100, A400);
6 |
7 | $theme: mat-light-theme($primary, $accent);
8 | @include angular-material-theme($theme);
9 | @include material-docs-app-theme($theme);
10 |
--------------------------------------------------------------------------------
/src/assets/img/component-categories/layout.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/movies/movie-detail/movie-detail.component.html:
--------------------------------------------------------------------------------
1 |
2 |
{{movie.name | uppercase}}
3 |
Id: {{movie.id}}
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | .main-container {
2 | display: flex;
3 | min-height: 100vh;
4 | flex-direction: column;
5 |
6 | app-navbar {
7 | position: fixed;
8 | top: 0;
9 | left: 0;
10 | right: 0;
11 | z-index: 2;
12 | }
13 |
14 | .site-content {
15 | flex: 1;
16 | margin-top: 56px;
17 | overflow-y: auto;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/shared/content-header/content-header.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-content-header',
5 | templateUrl: './content-header.component.html',
6 | styleUrls: ['./content-header.component.scss']
7 | })
8 | export class ContentHeaderComponent {
9 | @Input()
10 | title: string;
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/app",
5 | "types": []
6 | },
7 | "files": [
8 | "src/main.ts",
9 | "src/polyfills.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ],
14 | "exclude": [
15 | "src/test.ts",
16 | "src/**/*.spec.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": [
6 | "jasmine",
7 | "node"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/nav-bar.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-navbar',
5 | templateUrl: './nav-bar.component.html',
6 | styleUrls: ['./nav-bar.component.scss']
7 | })
8 | export class NavBarComponent {
9 |
10 | constructor() {
11 | }
12 |
13 | get sections() {
14 | return '';
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/assets/img/components/slide-toggle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/styles/custom-themes/pink-bluegrey.scss:
--------------------------------------------------------------------------------
1 | @import 'app-theme';
2 | @import '../../highlightjs/material-dark';
3 |
4 | // Define the dark theme.
5 | $primary: mat-palette($mat-pink);
6 | $accent: mat-palette($mat-blue-grey);
7 |
8 | $theme: mat-dark-theme($primary, $accent);
9 | @include angular-material-theme($theme);
10 | @include material-docs-app-theme($theme);
11 |
--------------------------------------------------------------------------------
/src/assets/img/components/button.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/styles/custom-themes/purple-green.scss:
--------------------------------------------------------------------------------
1 | @import 'app-theme';
2 | @import '../../highlightjs/material-dark';
3 |
4 | // Define the dark theme.
5 | $primary: mat-palette($mat-purple);
6 | $accent: mat-palette($mat-green, A200, A100, A400);
7 |
8 | $theme: mat-dark-theme($primary, $accent);
9 | @include angular-material-theme($theme);
10 | @include material-docs-app-theme($theme);
11 |
--------------------------------------------------------------------------------
/src/assets/img/examples/thumbup-icon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/shared/content-header/content-header.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {ContentHeaderComponent} from './content-header.component';
3 | import {CommonModule} from '@angular/common';
4 |
5 | @NgModule({
6 | imports: [CommonModule],
7 | exports: [ContentHeaderComponent],
8 | declarations: [ContentHeaderComponent],
9 | })
10 | export class ContentHeaderModule {
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-form-store/hero-form-store.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {StoreModule} from '@ngrx/store';
3 | import * as fromHeroForm from './hero-form.reducers';
4 |
5 | @NgModule({
6 | imports: [
7 | StoreModule.forFeature(fromHeroForm.HERO_FORM_ID, fromHeroForm.reducer),
8 | ],
9 | providers: [
10 | ]
11 | })
12 | export class HeroFormStoreModule {
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/shared/footer/_footer-theme.scss:
--------------------------------------------------------------------------------
1 | @mixin footer-theme($theme) {
2 | $primary: map-get($theme, primary);
3 | $accent: map-get($theme, accent);
4 | $warn: map-get($theme, warn);
5 | $background: map-get($theme, background);
6 | $foreground: map-get($theme, foreground);
7 |
8 | app-footer {
9 | .docs-footer-links a {
10 | color: mat-color($primary, default-contrast);
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import {enableProdMode} from '@angular/core';
2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
3 |
4 | import {AppModule} from './app/app.module';
5 | import {environment} from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(AppModule)
12 | .catch(err => console.error(err));
13 |
--------------------------------------------------------------------------------
/src/app/shared/content-header/content-header.component.scss:
--------------------------------------------------------------------------------
1 | @import '../../../styles/constants';
2 |
3 | .component-page-header {
4 | display: flex;
5 | align-items: center;
6 |
7 | h1 {
8 | outline: none;
9 |
10 | @media (max-width: $small-breakpoint-width) {
11 | padding: 1em 1em;
12 | font-size: 2em;
13 | }
14 | }
15 |
16 | @media (max-width: $small-breakpoint-width) {
17 | padding-left: 0;
18 | }
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/styles/_navbar-theme.scss:
--------------------------------------------------------------------------------
1 | @mixin nav-bar-theme($theme) {
2 | $primary: map-get($theme, primary);
3 | $accent: map-get($theme, accent);
4 | $warn: map-get($theme, warn);
5 | $background: map-get($theme, background);
6 | $foreground: map-get($theme, foreground);
7 |
8 | app-navbar {
9 | color: mat-color($primary, default-contrast);
10 |
11 | .docs-navbar, .docs-navbar-header {
12 | background: mat-color($primary);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | > 0.5%
9 | last 2 versions
10 | Firefox ESR
11 | not dead
12 | not IE 9-11 # For IE 9-11 support, remove 'not'.
--------------------------------------------------------------------------------
/src/app/shared/navbar/_navbar-theme.scss:
--------------------------------------------------------------------------------
1 | @mixin nav-bar-theme($theme) {
2 | $primary: map-get($theme, primary);
3 | $accent: map-get($theme, accent);
4 | $warn: map-get($theme, warn);
5 | $background: map-get($theme, background);
6 | $foreground: map-get($theme, foreground);
7 |
8 | app-navbar {
9 | color: mat-color($primary, default-contrast);
10 |
11 | .docs-navbar, .docs-navbar-header {
12 | background: mat-color($primary);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/styles/_component-sidenav-theme.scss:
--------------------------------------------------------------------------------
1 | @import '~@angular/material/theming';
2 | @import 'constants';
3 |
4 | @mixin component-viewer-sidenav-theme($theme) {
5 | $primary: map-get($theme, primary);
6 | $background: map-get($theme, background);
7 | $foreground: map-get($theme, foreground);
8 | $is-dark-theme: map-get($theme, is-dark);
9 | $nav-background-opacity: if($is-dark-theme, 0.2, 0.03);
10 | $nav-background-focus-opacity: if($is-dark-theme, 0.25, 0.08);
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/heroes/heroes-list/heroes-list.component.scss:
--------------------------------------------------------------------------------
1 | .heroes-list {
2 | display: flex;
3 | flex-wrap: wrap;
4 | padding: 20px;
5 | justify-content: center;
6 |
7 | .hero-item {
8 | margin: 20px;
9 | vertical-align: top;
10 | @media (min-width: 599px) {
11 | width: 280px;
12 | }
13 | mat-card {
14 | mat-card-content {
15 | @media (min-width: 599px) {
16 | min-height: 7.2em;
17 | }
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/assets/img/components/snack-bar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {RouterModule, Routes} from '@angular/router';
3 |
4 | export const appRoutes: Routes = [
5 | {path: 'heroes', loadChildren: './heroes/heroes.module#HeroesModule'},
6 | {path: 'movies', loadChildren: './movies/movies.module#MoviesModule'},
7 | ];
8 |
9 | @NgModule({
10 | imports: [
11 | RouterModule.forRoot(appRoutes)
12 | ],
13 | exports: [RouterModule]
14 | })
15 | export class AppRoutingModule {
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/nav-bar.component.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/src/assets/img/components/card.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/chips.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/sidenav.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "outDir": "./dist/out-tsc",
6 | "sourceMap": true,
7 | "declaration": false,
8 | "downlevelIteration": true,
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "importHelpers": true,
14 | "target": "es2015",
15 | "typeRoots": [
16 | "node_modules/@types"
17 | ],
18 | "lib": [
19 | "es2018",
20 | "dom"
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/img/favicons/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Angular Material",
3 | "short_name": "Angular Material",
4 | "theme_color": "#FFFFFF",
5 | "background_color": "#3F51B5",
6 | "start_url": "/",
7 | "display": "standalone",
8 | "icons": [
9 | {
10 | "src": "/assets/img/favicons/android-chrome-192x192.png?v=8.2.3",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "/assets/img/favicons/android-chrome-512x512.png?v=8.2.3",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/assets/img/components/toolbar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/dialog.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/nav-bar.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
2 | import {NavBarComponent} from './nav-bar.component';
3 | import {NavBarModule} from './nav-bar.module';
4 |
5 |
6 | describe('NavBar', () => {
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | imports: [NavBarModule],
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(NavBarComponent);
17 | fixture.detectChanges();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/app/heroes/hero-detail/hero-detail.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{hero.full_name}}
8 |
{{hero.description}}
9 |
10 |
11 |
12 |
13 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/movies/movies.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {FormsModule} from '@angular/forms';
4 | import {MoviesRoutingModule} from './movies-routing.module';
5 | import {MoviesListComponent} from './movies-list/movies-list.component';
6 | import {MovieDetailComponent} from './movie-detail/movie-detail.component';
7 |
8 | @NgModule({
9 | declarations: [
10 | MoviesListComponent,
11 | MovieDetailComponent,
12 | ],
13 | imports: [
14 | CommonModule,
15 | FormsModule,
16 | MoviesRoutingModule
17 | ]
18 | })
19 | export class MoviesModule {
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/nav-bar.component.scss:
--------------------------------------------------------------------------------
1 | .docs-navbar-header {
2 | display: flex;
3 | flex-wrap: wrap;
4 | align-items: center;
5 | padding: 8px 16px;
6 |
7 | > .mat-button {
8 | &:last-child {
9 | margin-left: auto;
10 | }
11 | }
12 | }
13 |
14 | .flex-spacer {
15 | flex-grow: 1;
16 | }
17 |
18 | .angular-logo {
19 | height: 26px;
20 | margin: 0 4px 3px 0;
21 | vertical-align: middle;
22 | }
23 |
24 | .docs-navbar-link {
25 | text-decoration: inherit;
26 | flex: 1;
27 | }
28 |
29 | app-theme-picker {
30 | display: none;
31 |
32 | @media (min-width: 328px) {
33 | display: block;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/assets/img/components/progress-spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/textarea.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TourOfHeroes
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/assets/img/theme-demo-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/movies/movies-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {RouterModule, Routes} from '@angular/router';
2 | import {NgModule} from '@angular/core';
3 | import {MoviesListComponent} from './movies-list/movies-list.component';
4 | import {MovieDetailComponent} from './movie-detail/movie-detail.component';
5 |
6 | const moviesRoutes: Routes = [
7 | {
8 | path: '', children: [
9 | {path: 'movies', component: MoviesListComponent},
10 | {path: ':id', component: MovieDetailComponent},
11 | ]
12 | },
13 | ];
14 |
15 | @NgModule({
16 | imports: [
17 | RouterModule.forChild(moviesRoutes)
18 | ],
19 | exports: [RouterModule]
20 | })
21 | export class MoviesRoutingModule {
22 | }
23 |
--------------------------------------------------------------------------------
/src/assets/img/components/tooltip.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/hero-store.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {StoreModule} from '@ngrx/store';
3 | import {EffectsModule} from '@ngrx/effects';
4 | import {HeroEffects} from './hero.effects';
5 | import * as fromHeroes from './hero.reducers';
6 | import {HeroResolver} from '../../resolvers/hero.resolver';
7 | import {HeroesResolver} from '../../resolvers/heroes.resolver';
8 |
9 | @NgModule({
10 | imports: [
11 | StoreModule.forFeature(fromHeroes.HERO_FEATURE_KEY, fromHeroes.reducer),
12 | EffectsModule.forFeature([HeroEffects]),
13 | ],
14 | providers: [
15 | HeroResolver,
16 | HeroesResolver
17 | ]
18 | })
19 | export class HeroStoreModule {
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/movies/movie-detail/movie-detail.component.css:
--------------------------------------------------------------------------------
1 | mat-card {
2 | width: 350px;
3 | }
4 |
5 | label {
6 | display: inline-block;
7 | width: 3em;
8 | margin: .5em 0;
9 | color: #607D8B;
10 | font-weight: bold;
11 | }
12 |
13 | input {
14 | height: 2em;
15 | font-size: 1em;
16 | padding-left: .4em;
17 | }
18 |
19 | button {
20 | margin-top: 20px;
21 | font-family: Arial;
22 | background-color: #eee;
23 | border: none;
24 | padding: 5px 10px;
25 | border-radius: 4px;
26 | cursor: pointer;
27 | cursor: hand;
28 | }
29 |
30 | button:hover {
31 | background-color: #cfd8dc;
32 | }
33 |
34 | button:disabled {
35 | background-color: #eee;
36 | color: #ccc;
37 | cursor: auto;
38 | }
39 |
--------------------------------------------------------------------------------
/src/assets/img/components/button-toggle.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/input.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 |
3 | import 'zone.js/dist/zone-testing';
4 | import {getTestBed} from '@angular/core/testing';
5 | import {
6 | BrowserDynamicTestingModule,
7 | platformBrowserDynamicTesting
8 | } from '@angular/platform-browser-dynamic/testing';
9 |
10 | declare const require: any;
11 |
12 | // First, initialize the Angular testing environment.
13 | getTestBed().initTestEnvironment(
14 | BrowserDynamicTestingModule,
15 | platformBrowserDynamicTesting()
16 | );
17 | // Then we find all the tests.
18 | const context = require.context('./', true, /\.spec\.ts$/);
19 | // And load the modules.
20 | context.keys().map(context);
21 |
--------------------------------------------------------------------------------
/src/assets/img/components/tabs.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/e2e/src/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { AppPage } from './app.po';
2 | import { browser, logging } from 'protractor';
3 |
4 | describe('workspace-project App', () => {
5 | let page: AppPage;
6 |
7 | beforeEach(() => {
8 | page = new AppPage();
9 | });
10 |
11 | it('should display welcome message', () => {
12 | page.navigateTo();
13 | expect(page.getTitleText()).toEqual('Welcome to tour-of-hero-detail-list!');
14 | });
15 |
16 | afterEach(async () => {
17 | // Assert that there are no errors emitted from the browser
18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER);
19 | expect(logs).not.toContain(jasmine.objectContaining({
20 | level: logging.Level.SEVERE,
21 | } as logging.Entry));
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/github-circle-white-transparent.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/img/components/progress-bar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/movies/movies-list/movies-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
List of Movies
3 |
4 |
5 |
8 |
9 |
12 |
13 |
14 |
24 |
25 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // This file can be replaced during build by using the `fileReplacements` array.
2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
3 | // The list of file replacements can be found in `angular.json`.
4 |
5 | export const environment = {
6 | production: false,
7 | appApi: {
8 | baseUrl: 'http://localhost:3000'
9 | }
10 | };
11 |
12 | /*
13 | * For easier debugging in development mode, you can import the following file
14 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
15 | *
16 | * This import should be commented out in production mode because it will have a negative impact
17 | * on performance if an error is thrown.
18 | */
19 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI.
20 |
--------------------------------------------------------------------------------
/src/app/shared/footer/footer.component.html:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/assets/img/components/list.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | # Only exists if Bazel was run
8 | /bazel-out
9 |
10 | # dependencies
11 | /node_modules
12 |
13 | # profiling files
14 | chrome-profiler-events.json
15 | speed-measure-plugin.json
16 |
17 | # IDEs and editors
18 | /.idea
19 | .project
20 | .classpath
21 | .c9/
22 | *.launch
23 | .settings/
24 | *.sublime-workspace
25 |
26 | # IDE - VSCode
27 | .vscode/*
28 | !.vscode/settings.json
29 | !.vscode/tasks.json
30 | !.vscode/launch.json
31 | !.vscode/extensions.json
32 | .history/*
33 |
34 | # misc
35 | /.sass-cache
36 | /connect.lock
37 | /coverage
38 | /libpeerconnection.log
39 | npm-debug.log
40 | yarn-error.log
41 | testem.log
42 | /typings
43 |
44 | # System Files
45 | .DS_Store
46 | Thumbs.db
47 |
--------------------------------------------------------------------------------
/src/assets/img/angular-material-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/movies/movies-list/movies-list.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core';
2 | import {Movie} from '../models/movie';
3 | import {MoviesStore} from '../movies.store';
4 |
5 | @Component({
6 | selector: 'app-movies-list',
7 | templateUrl: './movies-list.component.html',
8 | styleUrls: ['./movies-list.component.css'],
9 | providers: [
10 | MoviesStore
11 | ]
12 | })
13 | export class MoviesListComponent implements OnInit {
14 | movies$ = this.moviesStore.movies$;
15 |
16 | constructor(private readonly moviesStore: MoviesStore) {
17 | }
18 |
19 | ngOnInit() {
20 | this.moviesStore.setState({movies: []});
21 | }
22 |
23 | addMovie(name: string) {
24 | this.moviesStore.addMovie({name});
25 | }
26 |
27 | deleteMovie(movie: Movie) {
28 | // this.moviesStore.deleteMovie({movie});
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {TestBed, async} from '@angular/core/testing';
2 | import {AppComponent} from './app.component';
3 | import {BrowserModule} from '@angular/platform-browser';
4 | import {FormsModule} from '@angular/forms';
5 | import {RouterTestingModule} from '@angular/router/testing';
6 |
7 | describe('AppComponent', () => {
8 | beforeEach(async(() => {
9 | TestBed.configureTestingModule({
10 | declarations: [
11 | AppComponent
12 | ],
13 | imports: [
14 | BrowserModule,
15 | FormsModule,
16 | RouterTestingModule.withRoutes([])
17 | ]
18 | });
19 | }));
20 |
21 | it('should create the app', () => {
22 | const fixture = TestBed.createComponent(AppComponent);
23 | const app = fixture.debugElement.componentInstance;
24 | expect(app).toBeTruthy();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/theme-picker.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
16 |
--------------------------------------------------------------------------------
/src/app/movies/movie-detail/movie-detail.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {Movie} from '../models/movie';
3 | import {Observable} from 'rxjs';
4 | import {MoviesStore} from '../movies.store';
5 |
6 | @Component({
7 | selector: 'app-movie-detail',
8 | templateUrl: './movie-detail.component.html',
9 | styleUrls: ['./movie-detail.component.css']
10 | })
11 | export class MovieDetailComponent {
12 | movie$: Observable;
13 | name: string;
14 |
15 | @Input()
16 | set movieId(value: string) {
17 | // calls effect with value. 👇 Notice it's a single string value.
18 | this.moviesStore.getMovie(value);
19 | this.movie$ = this.moviesStore.selectMovie(value);
20 | }
21 |
22 | constructor(private readonly moviesStore: MoviesStore) {
23 | }
24 |
25 |
26 | onChangeName(val: string) {
27 | this.name = val;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/heroes/heroes-list/heroes-list.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{hero.name}}
7 | {{hero.full_name}}
8 |
9 |
10 |
11 | {{hero.description}}
12 |
13 |
14 | DETAIL
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/theme-picker.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {MatButtonModule} from '@angular/material/button';
4 | import {MatIconModule} from '@angular/material/icon';
5 | import {MatMenuModule} from '@angular/material/menu';
6 | import {MatTooltipModule} from '@angular/material/tooltip';
7 | import {ThemeService} from '../../services/theme.service';
8 | import {ThemePickerComponent} from './theme-picker.component';
9 | import {StyleService} from '../../services/style.service';
10 |
11 | @NgModule({
12 | imports: [
13 | CommonModule,
14 | MatButtonModule,
15 | MatIconModule,
16 | MatMenuModule,
17 | MatTooltipModule,
18 | ],
19 | exports: [ThemePickerComponent],
20 | declarations: [ThemePickerComponent],
21 | providers: [StyleService, ThemeService],
22 | })
23 | export class ThemePickerModule {
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/_general.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100vh;
4 | }
5 |
6 | body {
7 | font-family: "Roboto","Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;
8 | margin: 0;
9 | }
10 |
11 | .docs-button[md-button], .docs-button[md-raised-button] {
12 | text-transform: uppercase;
13 | }
14 |
15 | h1, h2 {
16 | font-weight: 400;
17 | }
18 |
19 | .docs-primary-header {
20 | padding-left: 20px;
21 |
22 | h1 {
23 | font-weight: 300;
24 | margin: 0;
25 | padding: 28px 8px;
26 | font-size: 20px;
27 | }
28 | }
29 |
30 | // These styles are for controlling SVGs without using the /deep/ selector
31 |
32 | .docs-component-category-list-card-image svg {
33 | width: 100%;
34 | }
35 |
36 | .docs-footer-angular-logo svg {
37 | height: 50px;
38 | }
39 |
40 | .angular-logo svg {
41 | height: 26px;
42 | margin: 0 4px 3px 0;
43 | vertical-align: middle;
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/shared/content-header/content-header.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
2 | import {ContentHeaderComponent} from './content-header.component';
3 | import {ContentHeaderModule} from './content-header.module';
4 |
5 |
6 | describe('ComponentPageHeader', () => {
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(waitForAsync(() => {
10 | TestBed.configureTestingModule({
11 | imports: [ContentHeaderModule],
12 | }).compileComponents();
13 | }));
14 |
15 | beforeEach(() => {
16 | fixture = TestBed.createComponent(ContentHeaderComponent);
17 | });
18 |
19 | it('should return the title', () => {
20 | const component = fixture.componentInstance;
21 | const title = 'foobar';
22 | fixture.detectChanges();
23 | component.title = title;
24 | expect(component.title).toEqual(title);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/app/heroes/heroes-routing.module.ts:
--------------------------------------------------------------------------------
1 | import {RouterModule, Routes} from '@angular/router';
2 | import {HeroesListComponent} from './heroes-list/heroes-list.component';
3 | import {HeroesResolver} from '../resolvers/heroes.resolver';
4 | import {HeroDetailComponent} from './hero-detail/hero-detail.component';
5 | import {HeroResolver} from '../resolvers/hero.resolver';
6 | import {NgModule} from '@angular/core';
7 |
8 | const heroesRoutes: Routes = [
9 | {
10 | path: '', children: [
11 | {path: '', redirectTo: 'heroes', pathMatch: 'full'},
12 | {path: 'heroes', component: HeroesListComponent, resolve: {entity: HeroesResolver}},
13 | {path: ':id', component: HeroDetailComponent, resolve: {entity: HeroResolver}},
14 | ]
15 | },
16 | ];
17 |
18 | @NgModule({
19 | imports: [
20 | RouterModule.forChild(heroesRoutes)
21 | ],
22 | exports: [RouterModule]
23 | })
24 | export class HeroesRoutingModule {
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/theme-picker.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {waitForAsync, TestBed} from '@angular/core/testing';
2 | import {ThemePickerComponent} from './theme-picker.component';
3 | import {ThemePickerModule} from './theme-picker.module';
4 |
5 |
6 | describe('ThemePicker', () => {
7 | beforeEach(waitForAsync(() => {
8 | TestBed.configureTestingModule({
9 | imports: [ThemePickerModule],
10 | }).compileComponents();
11 | }));
12 |
13 | it('should install theme based on name', () => {
14 | const fixture = TestBed.createComponent(ThemePickerComponent);
15 | const component = fixture.componentInstance;
16 | const name = 'pink-bluegrey';
17 | spyOn(component.styleManager, 'setStyle');
18 | component.selectTheme(name);
19 | expect(component.styleManager.setStyle).toHaveBeenCalled();
20 | expect(component.styleManager.setStyle).toHaveBeenCalledWith('theme', `assets/${name}.css`);
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/e2e/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | // Protractor configuration file, see link for more information
3 | // https://github.com/angular/protractor/blob/master/lib/config.ts
4 |
5 | const { SpecReporter } = require('jasmine-spec-reporter');
6 |
7 | /**
8 | * @type { import("protractor").Config }
9 | */
10 | exports.config = {
11 | allScriptsTimeout: 11000,
12 | specs: [
13 | './src/**/*.e2e-spec.ts'
14 | ],
15 | capabilities: {
16 | 'browserName': 'chrome'
17 | },
18 | directConnect: true,
19 | baseUrl: 'http://localhost:4200/',
20 | framework: 'jasmine',
21 | jasmineNodeOpts: {
22 | showColors: true,
23 | defaultTimeoutInterval: 30000,
24 | print: function() {}
25 | },
26 | onPrepare() {
27 | require('ts-node').register({
28 | project: require('path').join(__dirname, './tsconfig.json')
29 | });
30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
31 | }
32 | };
--------------------------------------------------------------------------------
/src/styles/_constants.scss:
--------------------------------------------------------------------------------
1 | @import '~@angular/material/theming';
2 |
3 | // The values were determined through the combination of Material Design breakpoints and careful
4 | // testing of the application across a range of common device widths (360px+).
5 | // These breakpoint values need to stay in sync with the related constants in
6 | // src/app/pages/component-sidenav/component-sidenav.ts.
7 | // The extra small breakpoint is used for styling the guides and certain aspects of the tables.
8 | $extra-small-breakpoint-width: 720px;
9 | // The small breakpoint is used for the component category and component list pages, the component
10 | // pages, the component sidenav, and certain aspects of the tables.
11 | $small-breakpoint-width: 959px;
12 |
13 | /* For desktop, the content should be aligned with the page title. */
14 | $content-padding-guides-side: 70px;
15 | $content-padding-sidenav: 50px;
16 | $content-padding-side-xs: 15px;
17 |
18 | $sidenav-width: 240px;
19 |
--------------------------------------------------------------------------------
/src/app/shared/navbar/nav-bar.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {HttpClientModule} from '@angular/common/http';
4 | import {MatButtonModule} from '@angular/material/button';
5 | import {MatMenuModule} from '@angular/material/menu';
6 | import {RouterModule} from '@angular/router';
7 | import {NavBarComponent} from './nav-bar.component';
8 | import {ThemePickerModule} from '../theme-picker/theme-picker.module';
9 | import {StyleService} from '../../services/style.service';
10 | import {ThemeService} from '../../services/theme.service';
11 |
12 | @NgModule({
13 | imports: [
14 | CommonModule,
15 | HttpClientModule,
16 | MatButtonModule,
17 | MatMenuModule,
18 | RouterModule,
19 | ThemePickerModule
20 | ],
21 | exports: [NavBarComponent],
22 | declarations: [NavBarComponent],
23 | providers: [StyleService, ThemeService]
24 | })
25 | export class NavBarModule {
26 | }
27 |
--------------------------------------------------------------------------------
/src/assets/img/components/select.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/heroes/hero-detail/hero-detail.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {select, Store} from '@ngrx/store';
3 | import {selectHero} from '../../root-store/hero-store';
4 | import * as fromHeroActions from '../../root-store/hero-store/hero.actions';
5 | import {Hero} from '../models';
6 |
7 | @Component({
8 | selector: 'app-hero',
9 | templateUrl: './hero-detail.component.html',
10 | styleUrls: ['./hero-detail.component.scss']
11 | })
12 | export class HeroDetailComponent {
13 | hero$ = this.store.pipe(select(selectHero));
14 | name: string;
15 |
16 | constructor(
17 | private store: Store
18 | ) {
19 | }
20 |
21 | goBack() {
22 | this.store.dispatch(fromHeroActions.navigateTo({prop: '/heroes'}));
23 | }
24 |
25 | update(val: Hero) {
26 | if (this.name) {
27 | const entity = {...val, name: this.name}
28 | this.store.dispatch(fromHeroActions.updateHero({entity}));
29 | }
30 | this.goBack();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/root-store/root-store.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {HeroStoreModule} from './hero-store';
4 | import {EffectsModule} from '@ngrx/effects';
5 | import {StoreModule} from '@ngrx/store';
6 | import {routerReducer, StoreRouterConnectingModule} from '@ngrx/router-store';
7 | import {StoreDevtoolsModule} from '@ngrx/store-devtools';
8 | import {environment} from '../../environments/environment';
9 | import {HeroFormStoreModule} from './hero-form-store';
10 |
11 | @NgModule({
12 | declarations: [],
13 | imports: [
14 | CommonModule,
15 | HeroStoreModule,
16 | HeroFormStoreModule,
17 | EffectsModule.forRoot([]),
18 | StoreModule.forRoot({
19 | router: routerReducer,
20 | }),
21 | StoreRouterConnectingModule.forRoot(),
22 | StoreDevtoolsModule.instrument({
23 | maxAge: 25,
24 | logOnly: environment.production
25 | }),
26 | ]
27 | })
28 | export class RootStoreModule {
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/services/heroes.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient} from '@angular/common/http';
3 | import {environment} from '../../environments/environment';
4 | import {Hero} from '../heroes/models';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class HeroesService {
10 | private readonly baseUrl: string;
11 |
12 | constructor(private http: HttpClient) {
13 | this.baseUrl = environment.appApi.baseUrl;
14 | }
15 |
16 | getAll() {
17 | return this.http.get(`${this.baseUrl}/heroes`);
18 | }
19 |
20 | get(id: string) {
21 | return this.http.get(`${this.baseUrl}/heroes/${id}`);
22 | }
23 |
24 | create(entity?: Hero) {
25 | return this.http.post(`${this.baseUrl}/heroes`, entity);
26 | }
27 |
28 | update(id: string, entity?: Hero) {
29 | return this.http.put(`${this.baseUrl}/heroes/${id}`, entity);
30 | }
31 |
32 | delete(id: string) {
33 | return this.http.delete(`${this.baseUrl}/heroes/${id}`);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/services/movies.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient} from '@angular/common/http';
3 | import {environment} from '../../environments/environment';
4 | import {Hero} from '../heroes/models';
5 |
6 | @Injectable({
7 | providedIn: 'root'
8 | })
9 | export class MoviesService {
10 | private readonly baseUrl: string;
11 |
12 | constructor(private http: HttpClient) {
13 | this.baseUrl = environment.appApi.baseUrl;
14 | }
15 |
16 | getAll() {
17 | return this.http.get(`${this.baseUrl}/movies`);
18 | }
19 |
20 | get(id: string) {
21 | return this.http.get(`${this.baseUrl}/movies/${id}`);
22 | }
23 |
24 | create(entity?: Hero) {
25 | return this.http.post(`${this.baseUrl}/movies`, entity);
26 | }
27 |
28 | update(id: string, entity?: Hero) {
29 | return this.http.put(`${this.baseUrl}/movies/${id}`, entity);
30 | }
31 |
32 | delete(id: string) {
33 | return this.http.delete(`${this.baseUrl}/movies/${id}`);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/resolvers/heroes.resolver.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Resolve} from '@angular/router';
3 | import {Store, select} from '@ngrx/store';
4 | import {Observable} from 'rxjs';
5 | import {filter, take} from 'rxjs/operators';
6 | import {HeroPartialState} from '../root-store/hero-store/hero.reducers';
7 | import {selectHeroesLoaded} from '../root-store/hero-store/hero.selectors';
8 | import * as fromHeroActions from '../root-store/hero-store/hero.actions';
9 |
10 | @Injectable()
11 | export class HeroesResolver implements Resolve {
12 | constructor(private store: Store) {
13 | }
14 |
15 | resolve(): Observable {
16 | const loaded$ = this.store.pipe(select(selectHeroesLoaded));
17 |
18 | return loaded$.pipe(
19 | filter(loaded => {
20 | if (!loaded) {
21 | this.store.dispatch(fromHeroActions.loadHeroes());
22 | }
23 |
24 | return loaded;
25 | }),
26 | take(1)
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/assets/img/components/autocomplete.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/shared/footer/footer.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
2 |
3 | import {FooterComponent, FooterModule} from './footer.component';
4 | import {DocsAppTestingModule} from '../../testing/testing-module';
5 |
6 |
7 | describe('Footer', () => {
8 | let fixture: ComponentFixture;
9 |
10 | beforeEach(waitForAsync(() => {
11 | TestBed.configureTestingModule({
12 | imports: [FooterModule, DocsAppTestingModule],
13 | }).compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(FooterComponent);
18 | fixture.detectChanges();
19 | });
20 |
21 | it('should have a link to angular.io', () => {
22 | const link = fixture.nativeElement.querySelector('.docs-footer-logo a');
23 | const href = link.getAttribute('href');
24 | const text = link.textContent;
25 | expect(href).toContain('angular.io');
26 | expect(text).toContain('Learn Angular');
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/assets/img/components/radio.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/services/theme.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable, EventEmitter} from '@angular/core';
2 |
3 | export interface DocsSiteTheme {
4 | name: string;
5 | displayName?: string;
6 | accent: string;
7 | primary: string;
8 | isDark?: boolean;
9 | isDefault?: boolean;
10 | }
11 |
12 |
13 | @Injectable()
14 | export class ThemeService {
15 | static storageKey = 'docs-theme-storage-current-name';
16 |
17 | onThemeUpdate: EventEmitter = new EventEmitter();
18 |
19 | storeTheme(theme: DocsSiteTheme) {
20 | try {
21 | window.localStorage[ThemeService.storageKey] = theme.name;
22 | } catch {
23 | }
24 |
25 | this.onThemeUpdate.emit(theme);
26 | }
27 |
28 | getStoredThemeName(): string | null {
29 | try {
30 | return window.localStorage[ThemeService.storageKey] || null;
31 | } catch {
32 | return null;
33 | }
34 | }
35 |
36 | clearStorage() {
37 | try {
38 | window.localStorage.removeItem(ThemeService.storageKey);
39 | } catch {
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/heroes/heroes.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {FlexLayoutModule} from '@angular/flex-layout';
4 | import {FormsModule} from '@angular/forms';
5 | import {HeroesRoutingModule} from './heroes-routing.module';
6 | import {HeroesListComponent} from './heroes-list/heroes-list.component';
7 | import {HeroDetailComponent} from './hero-detail/hero-detail.component';
8 | import {ContentHeaderModule} from '../shared/content-header/content-header.module';
9 | import {HeroFormComponent} from './hero-form/hero-form.component';
10 | import {NgrxFormsModule} from 'ngrx-forms';
11 | import {MaterialModule} from '../shared/material';
12 |
13 | @NgModule({
14 | declarations: [
15 | HeroesListComponent,
16 | HeroDetailComponent,
17 | HeroFormComponent
18 | ],
19 | imports: [
20 | CommonModule,
21 | FlexLayoutModule,
22 | FormsModule,
23 | HeroesRoutingModule,
24 | ContentHeaderModule,
25 | NgrxFormsModule,
26 | MaterialModule
27 | ]
28 | })
29 | export class HeroesModule {
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {NgModule} from '@angular/core';
3 | import {HttpClientModule} from '@angular/common/http';
4 | import {FlexLayoutModule} from '@angular/flex-layout';
5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
6 | import {AppComponent} from './app.component';
7 | import {RootStoreModule} from './root-store';
8 | import {HeroesModule} from './heroes';
9 | import {AppRoutingModule} from './app-routing.module';
10 | import {MoviesModule} from './movies/movies.module';
11 | import {NavBarModule} from './shared/navbar/nav-bar.module';
12 | import {FooterModule} from './shared/footer/footer.module';
13 |
14 | @NgModule({
15 | declarations: [
16 | AppComponent,
17 | ],
18 | imports: [
19 | BrowserModule,
20 | FlexLayoutModule,
21 | RootStoreModule,
22 | HeroesModule,
23 | NavBarModule,
24 | FooterModule,
25 | MoviesModule,
26 | HttpClientModule,
27 | AppRoutingModule,
28 | BrowserAnimationsModule
29 | ],
30 | bootstrap: [AppComponent]
31 | })
32 | export class AppModule {
33 | }
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TourOfHeroes
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.0.3.
4 |
5 | ## Development server
6 |
7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
8 |
9 | ## Code scaffolding
10 |
11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
12 |
13 | ## Build
14 |
15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
16 |
17 | ## Running unit tests
18 |
19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
20 |
21 | ## Running end-to-end tests
22 |
23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
24 |
25 | ## Further help
26 |
27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
28 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/angular-white-transparent.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/src/app/resolvers/hero.resolver.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Resolve, ActivatedRouteSnapshot} from '@angular/router';
3 | import {Store, select} from '@ngrx/store';
4 | import {Observable} from 'rxjs';
5 | import {filter, take} from 'rxjs/operators';
6 | import {Hero} from '../heroes/models';
7 | import {HeroPartialState} from '../root-store/hero-store/hero.reducers';
8 | import {selectHero} from '../root-store/hero-store/hero.selectors';
9 | import * as fromHeroActions from '../root-store/hero-store/hero.actions';
10 |
11 | @Injectable()
12 | export class HeroResolver implements Resolve {
13 | constructor(private store: Store) {
14 | }
15 |
16 | resolve(route: ActivatedRouteSnapshot): Observable {
17 | const entity$ = this.store.pipe(
18 | select(selectHero, {id: route.params.id})
19 | );
20 |
21 | return entity$.pipe(
22 | filter(entity => {
23 | if (!entity) {
24 | this.store.dispatch(fromHeroActions.loadHero({id: route.params.id}));
25 | }
26 |
27 | return !!entity;
28 | }),
29 | take(1)
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | client: {
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/tour-of-heroes'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true
22 | },
23 | reporters: ['progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | singleRun: false,
30 | restartOnFileChange: true
31 | });
32 | };
33 |
--------------------------------------------------------------------------------
/src/assets/img/components/checkbox.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/component-categories/buttons.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/components/slider.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/shared/material/error-state-matcher.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Host, Input, Optional } from '@angular/core';
2 | import { MatChipList } from '@angular/material/chips';
3 | import { MatInput } from '@angular/material/input';
4 | import { MatSelect } from '@angular/material/select';
5 | import { FormControlState } from 'ngrx-forms';
6 |
7 | @Directive({
8 | // tslint:disable-next-line:directive-selector
9 | selector: '[ngrxFormControlState]',
10 | })
11 | export class CustomErrorStateMatcherDirective {
12 | @Input() set ngrxFormControlState(state: FormControlState) {
13 | const errorsAreShown = state.isInvalid && (state.isTouched || state.isSubmitted);
14 |
15 | if (this.input) {
16 | this.input.errorState = errorsAreShown;
17 | this.input.stateChanges.next();
18 | }
19 |
20 | if (this.select) {
21 | this.select.errorState = errorsAreShown;
22 | this.select.stateChanges.next();
23 | }
24 |
25 | if (this.chipList) {
26 | this.chipList.errorState = errorsAreShown;
27 | this.chipList.stateChanges.next();
28 | }
29 | }
30 |
31 | constructor(
32 | @Host() @Optional() private input: MatInput,
33 | @Host() @Optional() private select: MatSelect,
34 | @Host() @Optional() private chipList: MatChipList,
35 | ) { }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/hero.selectors.ts:
--------------------------------------------------------------------------------
1 | import {createFeatureSelector, createSelector} from '@ngrx/store';
2 |
3 | import {State, adapter, HERO_FEATURE_KEY} from './hero.reducers';
4 |
5 | // Lookup the 'Hero' feature state managed by NgRx
6 | const getHeroesState = createFeatureSelector(HERO_FEATURE_KEY);
7 |
8 | // get the selectors
9 | const {selectIds, selectAll, selectTotal} = adapter.getSelectors();
10 |
11 | // select the array of Hero ids
12 | export const selectHeroIds = createSelector(
13 | getHeroesState,
14 | selectIds
15 | );
16 |
17 | // select the array of Heroes
18 | export const selectHeroes = createSelector(
19 | getHeroesState,
20 | selectAll
21 | );
22 |
23 | // select the total Hero count
24 | export const selectHeroCount = createSelector(
25 | getHeroesState,
26 | selectTotal
27 | );
28 |
29 | // select the Hero by Id
30 | export const selectHero = createSelector(
31 | getHeroesState,
32 | (state: State, prop: { id: string }) => state.entities[prop?.id]
33 | );
34 |
35 | // select entity loaded flag
36 | export const selectHeroesLoaded = createSelector(
37 | getHeroesState,
38 | state => state.loaded
39 | );
40 |
41 | // select entity error
42 | export const selectError = createSelector(
43 | getHeroesState,
44 | state => state.error
45 | );
46 |
--------------------------------------------------------------------------------
/src/_app-theme.scss:
--------------------------------------------------------------------------------
1 | @import '~@angular/material/theming';
2 |
3 | @import './styles/component-sidenav-theme';
4 | @import './styles/navbar-theme';
5 |
6 | // Styles for the docs app that are based on the current theme.
7 | @mixin material-docs-app-theme($theme) {
8 | $primary: map-get($theme, primary);
9 | $accent: map-get($theme, accent);
10 | $warn: map-get($theme, warn);
11 | $background: map-get($theme, background);
12 | $foreground: map-get($theme, foreground);
13 | $next-theme: mat-palette($mat-red);
14 |
15 | .docs-app-background {
16 | background: mat-color($background, background);
17 | }
18 |
19 | .docs-primary-header {
20 | background: mat-color($primary);
21 |
22 | h1 {
23 | color: mat-color($primary, default-contrast);
24 | }
25 | }
26 |
27 | .docs-footer {
28 | background: mat-color($primary);
29 | color: mat-color($primary, default-contrast);
30 | }
31 |
32 | .is-next-version {
33 | background: mat-color($next-theme, 900) !important;
34 | }
35 | @include component-viewer-sidenav-theme($theme);
36 | @include nav-bar-theme($theme);
37 | }
38 |
39 | // Define the light theme.
40 | $primary: mat-palette($mat-indigo);
41 | $accent: mat-palette($mat-pink, A200, A100, A400);
42 |
43 |
44 | $theme: mat-light-theme($primary, $accent);
45 |
--------------------------------------------------------------------------------
/src/app/heroes/hero-form/hero-form.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {Store} from '@ngrx/store';
3 | import {ResetAction, SetValueAction} from 'ngrx-forms';
4 | import {filter, map, take} from 'rxjs/operators';
5 | import {initialHeroFormState, SetSubmittedValueAction} from '../../root-store/hero-form-store/hero-form.reducers';
6 | import * as fromHeroActions from '../../root-store/hero-store/hero.actions';
7 |
8 | @Component({
9 | selector: 'app-hero-form',
10 | templateUrl: './hero-form.component.html',
11 | styleUrls: ['./hero-form.component.scss']
12 | })
13 | export class HeroFormComponent {
14 | heroFormState$ = this.store.select(s => s.heroForm.formState);
15 | // submittedValue$ = this.store.select(s => s.heroForm.submittedValue);
16 |
17 | constructor(
18 | private store: Store
19 | ) {
20 | }
21 |
22 | reset() {
23 | this.store.dispatch(new SetValueAction(initialHeroFormState.id, initialHeroFormState.value));
24 | this.store.dispatch(new ResetAction(initialHeroFormState.id));
25 | }
26 |
27 | submit() {
28 | this.heroFormState$.pipe(
29 | take(1),
30 | filter(s => s.isValid),
31 | map(fs => {
32 | this.store.dispatch(fromHeroActions.updateHero({entity: fs.value}));
33 | return new SetSubmittedValueAction(fs.value)
34 | }),
35 | ).subscribe(this.store);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/services/style.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 |
3 |
4 | /**
5 | * Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be
6 | * removed or changed later.
7 | */
8 | @Injectable()
9 | export class StyleService {
10 | /**
11 | * Set the stylesheet with the specified key.
12 | */
13 | setStyle(key: string, href: string) {
14 | getLinkElementForKey(key).setAttribute('href', href);
15 | }
16 |
17 | /**
18 | * Remove the stylesheet with the specified key.
19 | */
20 | removeStyle(key: string) {
21 | const existingLinkElement = getExistingLinkElementByKey(key);
22 | if (existingLinkElement) {
23 | document.head.removeChild(existingLinkElement);
24 | }
25 | }
26 | }
27 |
28 | function getLinkElementForKey(key: string) {
29 | return getExistingLinkElementByKey(key) || createLinkElementWithKey(key);
30 | }
31 |
32 | function getExistingLinkElementByKey(key: string) {
33 | return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`);
34 | }
35 |
36 | function createLinkElementWithKey(key: string) {
37 | const linkEl = document.createElement('link');
38 | linkEl.setAttribute('rel', 'stylesheet');
39 | linkEl.classList.add(getClassNameForKey(key));
40 | document.head.appendChild(linkEl);
41 | return linkEl;
42 | }
43 |
44 | function getClassNameForKey(key: string) {
45 | return `style-manager-${key}`;
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/heroes/heroes-list/heroes-list.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {select, Store} from '@ngrx/store';
3 | import {selectHeroes} from '../../root-store/hero-store/hero.selectors';
4 | import * as fromHeroActions from '../../root-store/hero-store/hero.actions';
5 | import {Hero} from '../models';
6 | import {MatDialog} from '@angular/material/dialog';
7 | import {HeroFormComponent} from '../hero-form/hero-form.component';
8 | import {SetValueAction} from 'ngrx-forms';
9 |
10 | @Component({
11 | selector: 'app-heroes',
12 | templateUrl: './heroes-list.component.html',
13 | styleUrls: ['./heroes-list.component.scss']
14 | })
15 | export class HeroesListComponent {
16 | headerTitle = 'Heroes list';
17 | heroes$ = this.store.pipe(select(selectHeroes));
18 |
19 | constructor(
20 | private store: Store,
21 | public dialog: MatDialog
22 | ) {
23 | }
24 |
25 | addHero(hero: Hero) {
26 | this.store.dispatch(fromHeroActions.createHero({entity: {...hero}}));
27 | }
28 |
29 | editHero(hero: Hero) {
30 | this.store.dispatch(new SetValueAction('heroForm', hero));
31 |
32 | const dialogRef = this.dialog.open(HeroFormComponent);
33 | dialogRef.afterClosed().subscribe(result => {
34 | });
35 | // this.store.dispatch(fromHeroActions.createHero({entity: {name}}));
36 | }
37 |
38 | deleteHero(hero: Hero) {
39 | this.store.dispatch(fromHeroActions.deleteHero({id: hero.id}));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/img/favicons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/movies/movies-list/movies-list.component.css:
--------------------------------------------------------------------------------
1 | /* HeroListComponent's private CSS styles */
2 | .movies {
3 | margin: 0 0 2em 0;
4 | list-style-type: none;
5 | padding: 0;
6 | width: 15em;
7 | }
8 |
9 | .movies li {
10 | position: relative;
11 | cursor: pointer;
12 | background-color: #EEE;
13 | margin: .5em;
14 | padding: .3em 0;
15 | height: 1.6em;
16 | border-radius: 4px;
17 | }
18 |
19 | .movies li:hover {
20 | color: #607D8B;
21 | background-color: #DDD;
22 | left: .1em;
23 | }
24 |
25 | .movies a {
26 | color: #333;
27 | text-decoration: none;
28 | position: relative;
29 | display: block;
30 | width: 250px;
31 | }
32 |
33 | .movies a:hover {
34 | color: #607D8B;
35 | }
36 |
37 | .movies .badge {
38 | display: inline-block;
39 | font-size: small;
40 | color: white;
41 | padding: 0.8em 0.7em 0 0.7em;
42 | background-color: #405061;
43 | line-height: 1em;
44 | position: relative;
45 | left: -1px;
46 | top: -4px;
47 | height: 1.8em;
48 | min-width: 16px;
49 | text-align: right;
50 | margin-right: .8em;
51 | border-radius: 4px 0 0 4px;
52 | }
53 |
54 | button {
55 | background-color: #eee;
56 | border: none;
57 | padding: 5px 10px;
58 | border-radius: 4px;
59 | cursor: pointer;
60 | cursor: hand;
61 | font-family: Arial;
62 | }
63 |
64 | button:hover {
65 | background-color: #cfd8dc;
66 | }
67 |
68 | button.delete {
69 | position: relative;
70 | left: 194px;
71 | top: -32px;
72 | background-color: gray !important;
73 | color: white;
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/src/app/shared/footer/footer.component.scss:
--------------------------------------------------------------------------------
1 | .docs-footer {
2 | padding: 12px;
3 | font-size: 12px;
4 | }
5 |
6 | .docs-footer-list {
7 | align-items: center;
8 | display: flex;
9 | flex-flow: row wrap;
10 | padding: 8px;
11 | }
12 |
13 | .docs-footer-logo {
14 | flex: 1;
15 | }
16 |
17 | .docs-footer-angular-logo {
18 | height: 50px;
19 | }
20 |
21 | .docs-footer-version {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | flex: 1;
26 |
27 | .angular-material-logo {
28 | height: 40px;
29 | margin: 10px 0 10px 16px;
30 | }
31 |
32 | .version {
33 | margin: 0 40px;
34 | }
35 | }
36 |
37 | .docs-footer-copyright {
38 | display: flex;
39 | flex: 1;
40 | justify-content: flex-end;
41 | flex-direction: column;
42 | min-width: 225px;
43 | margin-top: 16px;
44 |
45 | > div {
46 | display: flex;
47 | flex-direction: column;
48 | align-self: flex-end;
49 | text-align: center;
50 | }
51 |
52 | @media (min-width: 885px) {
53 | margin-top: 0;
54 | }
55 | }
56 |
57 | .docs-footer-logo span {
58 | display: inline-block;
59 | line-height: 50px;
60 | margin: 0 40px;
61 | vertical-align: bottom;
62 |
63 | a {
64 | font-size: 16px;
65 | padding: 0;
66 | }
67 | }
68 |
69 | a {
70 | text-decoration: none;
71 | color: inherit;
72 |
73 | &:hover,
74 | &:focus {
75 | text-decoration: underline;
76 | }
77 | }
78 |
79 | @media screen and (max-width: 884px) {
80 | .docs-footer-list {
81 | flex-direction: column;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/app/movies/movies.store.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {ComponentStore} from '@ngrx/component-store';
3 | import {Movie} from './models/movie';
4 | import {Observable} from 'rxjs';
5 | import {catchError, switchMap, tap} from 'rxjs/operators';
6 | import {MoviesService} from '../services/movies.service';
7 |
8 | export interface MoviesState {
9 | movies: Movie[];
10 | }
11 |
12 | @Injectable()
13 | export class MoviesStore extends ComponentStore {
14 |
15 | constructor(private readonly moviesService: MoviesService) {
16 | super({movies: []});
17 | }
18 |
19 | readonly movies$ = this.select(state => state.movies);
20 |
21 | // Each new call of getMovie(id) pushed that id into movieId$ stream.
22 | readonly getMovie = this.effect((movieId$: Observable) => {
23 | return movieId$.pipe(
24 | // Handle race condition with the proper choice of the flattening operator.
25 | switchMap((id) => this.moviesService.get(id).pipe(
26 | // Act on the result within inner pipe.
27 | tap({
28 | next: (movie: any) => this.addMovie(movie),
29 | error: (e) => console.log(e),
30 | }),
31 | // Handle potential error within inner pipe.
32 | catchError((err) => err),
33 | ))
34 | );
35 | })
36 |
37 | readonly addMovie = this.updater((state, movie: Movie) => ({
38 | movies: [...state.movies, movie]
39 | }));
40 |
41 | selectMovie(movieId: string) {
42 | return this.select((state) => state.movies.find(m => m.id === movieId));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/app/heroes/hero-detail/hero-detail.component.scss:
--------------------------------------------------------------------------------
1 | .hero-detail {
2 | width: 100%;
3 |
4 | .hero-banner {
5 | min-height: 410px;
6 | max-height: 640px;
7 | height: 50vh;
8 | order: inherit;
9 |
10 | .hero-background-photo {
11 | width: 100%;
12 | height: 100%;
13 | background-position: top;
14 | background-position-x: center;
15 | background-position-y: top;
16 | background-size: cover;
17 | background-repeat: no-repeat;
18 |
19 | .hero-description-container {
20 | margin: 0 auto;
21 | height: 100%;
22 | width: 100%;
23 | line-height: 1.7;
24 | flex-direction: column;
25 | box-sizing: border-box;
26 | display: flex;
27 | place-content: flex-end;
28 | @media (min-width: 599px) {
29 | place-content: center;
30 | }
31 |
32 | .hero-description {
33 | color: white;
34 | padding: 20px;
35 | background-color:rgba(0, 0, 0, 0.5);
36 |
37 | @media (min-width: 599px) {
38 | width: 40%;
39 | padding-left: 100px;
40 | }
41 |
42 | h1 {
43 | font-size: 2em;
44 | }
45 |
46 | div {
47 | font-size: 1.5em;
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
54 | .hero-text {
55 | max-width: 400px;
56 | padding: 20px;
57 | font-size: 1.2em;
58 | line-height: 1.5em;
59 |
60 | @media (min-width: 599px) {
61 | padding: 50px 50px 50px 200px;
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/theme-picker.component.scss:
--------------------------------------------------------------------------------
1 | .docs-theme-picker-menu {
2 | .mat-menu-item {
3 | .mat-icon.theme-example-icon {
4 | margin-right: 0;
5 | margin-left: 8px;
6 | float: right;
7 |
8 | svg {
9 | vertical-align: middle;
10 | }
11 |
12 | // The below colors need to align with the themes defined in ThemePicker
13 | &.deeppurple-amber svg {
14 | .docs-theme-icon-background {
15 | fill: #fafafa;
16 | }
17 |
18 | .docs-theme-icon-button {
19 | fill: #FFC107;
20 | }
21 |
22 | .docs-theme-icon-toolbar {
23 | fill: #673AB7;
24 | }
25 | }
26 |
27 | &.indigo-pink svg {
28 | .docs-theme-icon-background {
29 | fill: #fafafa;
30 | }
31 |
32 | .docs-theme-icon-button {
33 | fill: #E91E63;
34 | }
35 |
36 | .docs-theme-icon-toolbar {
37 | fill: #3F51B5;
38 | }
39 | }
40 |
41 | &.pink-bluegrey svg {
42 | .docs-theme-icon-background {
43 | fill: #303030;
44 | }
45 |
46 | .docs-theme-icon-button {
47 | fill: #607D8B;
48 | }
49 |
50 | .docs-theme-icon-toolbar {
51 | fill: #E91E63;
52 | }
53 | }
54 |
55 | &.purple-green svg {
56 | .docs-theme-icon-background {
57 | fill: #303030;
58 | }
59 |
60 | .docs-theme-icon-button {
61 | fill: #4CAF50;
62 | }
63 |
64 | .docs-theme-icon-toolbar {
65 | fill: #9C27B0;
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/assets/img/component-categories/modals.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/services/theme.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {ThemeService, DocsSiteTheme} from './theme.service';
2 |
3 |
4 | const testStorageKey = ThemeService.storageKey;
5 | const testTheme: DocsSiteTheme = {
6 | primary: '#000000',
7 | accent: '#ffffff',
8 | name: 'test-theme'
9 | };
10 |
11 | describe('ThemeService Service', () => {
12 | const service = new ThemeService();
13 | const getCurrTheme = () => window.localStorage.getItem(testStorageKey);
14 | const secondTestTheme = {
15 | primary: '#666666',
16 | accent: '#333333',
17 | name: 'other-test-theme'
18 | };
19 |
20 | beforeEach(() => {
21 | window.localStorage[testStorageKey] = testTheme.name;
22 | });
23 |
24 | afterEach(() => {
25 | window.localStorage.clear();
26 | });
27 |
28 | it('should set the current theme name', () => {
29 | expect(getCurrTheme()).toEqual(testTheme.name);
30 | service.storeTheme(secondTestTheme);
31 | expect(getCurrTheme()).toEqual(secondTestTheme.name);
32 | });
33 |
34 | it('should get the current theme name', () => {
35 | const theme = service.getStoredThemeName();
36 | expect(theme).toEqual(testTheme.name);
37 | });
38 |
39 | it('should clear the stored theme data', () => {
40 | expect(getCurrTheme()).not.toBeNull();
41 | service.clearStorage();
42 | expect(getCurrTheme()).toBeNull();
43 | });
44 |
45 | it('should emit an event when setTheme is called', () => {
46 | spyOn(service.onThemeUpdate, 'emit');
47 | service.storeTheme(secondTestTheme);
48 | expect(service.onThemeUpdate.emit).toHaveBeenCalled();
49 | expect(service.onThemeUpdate.emit).toHaveBeenCalledWith(secondTestTheme);
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/highlightjs/material-light.scss:
--------------------------------------------------------------------------------
1 | /** Adapted from https://github.com/atom-material/atom-material-syntax-light */
2 | .hljs {
3 | display: block;
4 | overflow-x: auto;
5 | padding: 1em;
6 | background: #FAFAFA;
7 | color: #37474f;
8 | -webkit-font-smoothing: antialiased;
9 | text-size-adjust: 100%;
10 | font: 300 100%/1 Roboto Mono,monospace;
11 | font-size: 14px;
12 | }
13 |
14 | .hljs > *::selection,
15 | .hljs-section {
16 | background-color: #D6EDEA;
17 | }
18 |
19 | .hljs-comment {
20 | color: #616161;
21 | font-style: italic;
22 | }
23 |
24 | .hljs-tag,
25 | .hljs-selector-tag,
26 | .hljs-regexp,
27 | .hljs-meta {
28 | color: #9c27b0;
29 | }
30 |
31 | .hljs-string,
32 | .hljs-subst {
33 | color: #0d904f;
34 | }
35 |
36 | .hljs-number,
37 | .hljs-variable,
38 | .hljs-template-variable {
39 | color: #80CBC4;
40 | }
41 |
42 | .hljs-name,
43 | .hljs-keyword,
44 | .hljs-type,
45 | .hljs-attribute {
46 | color: #3b78e7;
47 | }
48 |
49 | .hljs-title,
50 | .hljs-function > .hljs-title,
51 | .hljs-symbol,
52 | .hljs-bullet,
53 | .hljs-built_in,
54 | .hljs-builtin-name,
55 | .hljs-link {
56 | color: #6182B8;
57 | }
58 |
59 | .hljs-params {
60 | color: #d81b60;
61 | }
62 |
63 | .hljs-addition {
64 | color: #3b78e7;
65 | display: inline-block;
66 | width: 100%;
67 | }
68 |
69 | .hljs-deletion {
70 | color: #E53935;
71 | display: inline-block;
72 | width: 100%;
73 | }
74 |
75 | .hljs-selector-id,
76 | .hljs-selector-class {
77 | color: #8796B0;
78 | }
79 |
80 | .hljs-emphasis {
81 | font-style: italic;
82 | }
83 |
84 | .hljs-strong {
85 | font-weight: bold;
86 | }
87 |
88 | .hljs-link {
89 | text-decoration: underline;
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/shared/material/mat-list-option-fix.ts:
--------------------------------------------------------------------------------
1 |
2 | import { Directive, Host, OnDestroy } from '@angular/core';
3 | import { MatListOption } from '@angular/material/list';
4 |
5 | /**
6 | * This directive fixes an issue with the selection list component
7 | * which causes the form state to lose all values when the selection
8 | * list component leaves the DOM. The reason for that is that the
9 | * list options de-register themselves when being destroyed which
10 | * forces the list to update its value (which then does not contain
11 | * the removed option anymore). This behaviour usually makes sense
12 | * but as a side-effect it also resets the value of the list when the
13 | * list itself is destroyed since all its child options are destroyed
14 | * first. This directive is a workaround for that by preventing the
15 | * option from reporting the value change after it is destroyed. This
16 | * only works because reporting the value change is deferred inside
17 | * the ngOnDestroy function of the MatListOption which allows us to
18 | * run the code below in between the option being destroyed and it
19 | * trying to report the value change to its parent list (this has been
20 | * observed up until v5.2.5 of @angular/material, therefore there is
21 | * no guarantee that this workaround will continue to work).
22 | */
23 | @Directive({
24 | // tslint:disable-next-line:directive-selector
25 | selector: 'mat-list-option',
26 | })
27 | export class MatListOptionFixDirective implements OnDestroy {
28 | constructor(@Host() private matDirective: MatListOption) { }
29 |
30 | ngOnDestroy() {
31 | this.matDirective.selectionList = { _reportValueChange: () => void 0 } as any;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/assets/img/components/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tour-of-heroes",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "test": "ng test",
9 | "lint": "ng lint",
10 | "e2e": "ng e2e"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "~11.2.0",
15 | "@angular/cdk": "^11.2.0",
16 | "@angular/common": "~11.2.0",
17 | "@angular/compiler": "~11.2.0",
18 | "@angular/core": "~11.2.0",
19 | "@angular/flex-layout": "^11.0.0-beta.33",
20 | "@angular/forms": "~11.2.0",
21 | "@angular/material": "^11.2.0",
22 | "@angular/platform-browser": "~11.2.0",
23 | "@angular/platform-browser-dynamic": "~11.2.0",
24 | "@angular/router": "~11.2.0",
25 | "@ngrx/component-store": "^11.0.0",
26 | "@ngrx/data": "^11.0.0",
27 | "@ngrx/effects": "^11.0.0",
28 | "@ngrx/entity": "^11.0.0",
29 | "@ngrx/router-store": "^11.0.0",
30 | "@ngrx/store": "^11.0.0",
31 | "@ngrx/store-devtools": "^11.0.0",
32 | "ngrx-forms": "^6.3.4",
33 | "rxjs": "~6.6.0",
34 | "tslib": "^2.0.0",
35 | "zone.js": "~0.11.3"
36 | },
37 | "devDependencies": {
38 | "@angular-devkit/build-angular": "~0.1102.0",
39 | "@angular/cli": "~11.2.0",
40 | "@angular/compiler-cli": "~11.2.0",
41 | "@angular/language-service": "^11.2.0",
42 | "@types/jasmine": "~3.6.0",
43 | "@types/jasminewd2": "^2.0.8",
44 | "@types/node": "^12.11.1",
45 | "codelyzer": "^6.0.0",
46 | "jasmine-core": "^3.6.0",
47 | "jasmine-spec-reporter": "^5.0.0",
48 | "karma": "~6.1.0",
49 | "karma-chrome-launcher": "~3.1.0",
50 | "karma-coverage": "~2.0.3",
51 | "karma-jasmine": "~4.0.0",
52 | "karma-jasmine-html-reporter": "^1.5.0",
53 | "protractor": "^7.0.0",
54 | "ts-node": "~8.3.0",
55 | "tslint": "~6.1.0",
56 | "typescript": "~4.1.2"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-form-store/hero-form.reducers.ts:
--------------------------------------------------------------------------------
1 | import {Action, combineReducers} from '@ngrx/store';
2 | import {
3 | createFormGroupState,
4 | createFormStateReducerWithUpdate,
5 | FormGroupState,
6 | updateGroup,
7 | validate
8 | } from 'ngrx-forms';
9 | import { required } from 'ngrx-forms/validation';
10 |
11 | export const HERO_FORM_ID = 'heroForm';
12 |
13 | export interface HeroFormValue {
14 | id?: string;
15 | name: string;
16 | full_name: string;
17 | thumb: string;
18 | photo: string;
19 | description: string;
20 | text: string;
21 | }
22 |
23 | export interface State {
24 | heroForm: {
25 | formState: FormGroupState;
26 | submittedValue: HeroFormValue | undefined;
27 | };
28 | }
29 |
30 | export class SetSubmittedValueAction implements Action {
31 | static readonly TYPE = 'material/SET_SUBMITTED_VALUE';
32 | readonly type = SetSubmittedValueAction.TYPE;
33 | constructor(public submittedValue: HeroFormValue) { }
34 | }
35 |
36 | export const initialHeroFormState = createFormGroupState(HERO_FORM_ID, {
37 | name: '',
38 | full_name: '',
39 | thumb: '',
40 | photo: '',
41 | description: '',
42 | text: ''
43 | });
44 |
45 | const validationFormGroupReducer = createFormStateReducerWithUpdate(updateGroup({
46 | name: validate(required),
47 | full_name: validate(required),
48 | thumb: validate(required),
49 | photo: validate(required),
50 | description: validate(required)
51 | }));
52 |
53 | const reducers = combineReducers({
54 | formState(s = initialHeroFormState, a: Action) {
55 | return validationFormGroupReducer(s, a);
56 | },
57 | submittedValue(s: HeroFormValue | undefined, a: SetSubmittedValueAction) {
58 | switch (a.type) {
59 | case SetSubmittedValueAction.TYPE:
60 | return a.submittedValue;
61 | default:
62 | return s;
63 | }
64 | },
65 | });
66 |
67 | export function reducer(state: State['heroForm'], action: Action) {
68 | return reducers(state, action);
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/services/style.service.spec.ts:
--------------------------------------------------------------------------------
1 | import {HttpClientTestingModule} from '@angular/common/http/testing';
2 | import {inject, TestBed} from '@angular/core/testing';
3 | import {StyleService} from './style.service';
4 |
5 |
6 | describe('StyleService', () => {
7 | let styleService: StyleService;
8 |
9 | beforeEach(() => TestBed.configureTestingModule({
10 | imports: [HttpClientTestingModule],
11 | providers: [StyleService]
12 | }));
13 |
14 | beforeEach(inject([StyleService], (sm: StyleService) => {
15 | styleService = sm;
16 | }));
17 |
18 | afterEach(() => {
19 | const links = document.head.querySelectorAll('link');
20 | for (const link of Array.prototype.slice.call(links)) {
21 | if (link.className.includes('style-manager-')) {
22 | document.head.removeChild(link);
23 | }
24 | }
25 | });
26 |
27 | it('should add stylesheet to head', () => {
28 | styleService.setStyle('test', 'test.css');
29 | const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
30 | expect(styleEl).not.toBeNull();
31 | expect(styleEl.href.endsWith('test.css')).toBe(true);
32 | });
33 |
34 | it('should change existing stylesheet', () => {
35 | styleService.setStyle('test', 'test.css');
36 | const styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
37 | expect(styleEl).not.toBeNull();
38 | expect(styleEl.href.endsWith('test.css')).toBe(true);
39 |
40 | styleService.setStyle('test', 'new.css');
41 | expect(styleEl.href.endsWith('new.css')).toBe(true);
42 | });
43 |
44 | it('should remove existing stylesheet', () => {
45 | styleService.setStyle('test', 'test.css');
46 | let styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
47 | expect(styleEl).not.toBeNull();
48 | expect(styleEl.href.endsWith('test.css')).toBe(true);
49 |
50 | styleService.removeStyle('test');
51 | styleEl = document.head.querySelector('.style-manager-test') as HTMLLinkElement;
52 | expect(styleEl).toBeNull();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/hero.actions.ts:
--------------------------------------------------------------------------------
1 | import {createAction, props} from '@ngrx/store';
2 | import {Hero} from '../../heroes/models';
3 |
4 | export const loadHeroes = createAction(
5 | '[Hero] Load Heroes'
6 | );
7 |
8 | export const loadHeroesSuccess = createAction(
9 | '[Hero] Load Heroes Success',
10 | props<{ data: Hero[] }>()
11 | );
12 |
13 | export const loadHeroesFail = createAction(
14 | '[Hero] Load Heroes Fail',
15 | props<{ error: Error | any }>()
16 | );
17 |
18 | export const loadHero = createAction(
19 | '[Hero] Load Hero',
20 | props<{ id: string }>()
21 | );
22 |
23 | export const loadHeroSuccess = createAction(
24 | '[Hero] Load Hero Success',
25 | props<{ entity: Hero }>()
26 | );
27 |
28 | export const loadHeroFail = createAction(
29 | '[Hero] Load Hero Fail',
30 | props<{ error: Error | any }>()
31 | );
32 |
33 | export const updateHero = createAction(
34 | '[Hero] Update Hero',
35 | props<{ entity: Hero }>()
36 | );
37 |
38 | export const updateHeroSuccess = createAction(
39 | '[Hero] Update Hero Success',
40 | props<{ entity: Hero }>()
41 | );
42 |
43 | export const updateHeroFail = createAction(
44 | '[Hero] Update Hero Fail',
45 | props<{ error: Error | any }>()
46 | );
47 |
48 | export const createHero = createAction(
49 | '[Hero] Create Hero',
50 | props<{ entity: Hero }>()
51 | );
52 |
53 | export const createHeroSuccess = createAction(
54 | '[Hero] Create Hero Success',
55 | props<{ entity: Hero }>()
56 | );
57 |
58 | export const createHeroFail = createAction(
59 | '[Hero] Create Hero Fail',
60 | props<{ error: Error | any }>()
61 | );
62 |
63 | export const deleteHero = createAction(
64 | '[Hero] Delete Hero',
65 | props<{ id: string }>()
66 | );
67 |
68 | export const deleteHeroSuccess = createAction(
69 | '[Hero] Delete Hero Success',
70 | props<{ id: string }>()
71 | );
72 |
73 | export const deleteHeroFail = createAction(
74 | '[Hero] Delete Hero Fail',
75 | props<{ error: Error | any }>()
76 | );
77 |
78 | export const navigateTo = createAction(
79 | '[Router] Navigate to',
80 | props<{ prop: string }>()
81 | );
82 |
--------------------------------------------------------------------------------
/src/assets/img/component-categories/forms.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/highlightjs/material-dark.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Orginal Style from https://github.com/Kelbster/highlightjs-material-dark-theme (c) Kelby Gassmanl
3 | */
4 | .hljs {
5 | display: block;
6 | overflow-x: auto;
7 | padding: 1em;
8 | background: #2B2B2D;
9 | color: #CDD3D8;
10 | -webkit-font-smoothing: antialiased;
11 | text-size-adjust: 100%;
12 | font: 300 100%/1 Roboto Mono,monospace;
13 | font-size: 14px;
14 | }
15 |
16 | .hljs > *::selection {
17 | background-color: #3e4451;
18 | }
19 |
20 | .hljs-comment {
21 | color: #9E9E9E;
22 | font-style: italic;
23 | }
24 |
25 | .hljs-selector-tag {
26 | color: #C792EA;
27 | }
28 |
29 | .hljs-string,
30 | .hljs-subst {
31 | color: #9ccc65;
32 | }
33 |
34 | .hljs-number,
35 | .hljs-regexp,
36 | .hljs-variable,
37 | .hljs-template-variable {
38 | color: #F77669;
39 | }
40 |
41 | .hljs-keyword {
42 | color: #C792EA;
43 | }
44 | .hljs-function > .hljs-title {
45 | color: #75A5FF;
46 | }
47 | .hljs-tag {
48 | color: #ce93d8;
49 | }
50 | .hljs-name {
51 | color: #4dd0e1;
52 | }
53 | .hljs-type {
54 | color: #da4939;
55 | }
56 |
57 | .hljs-attribute {
58 | color: #80CBBF;
59 | }
60 |
61 | .hljs-symbol,
62 | .hljs-bullet,
63 | .hljs-built_in,
64 | .hljs-builtin-name,
65 | .hljs-link {
66 | color: #C792EA;
67 | }
68 |
69 | .hljs-params {
70 | color: #EEFFF7;
71 | }
72 |
73 |
74 | .hljs-meta {
75 | color: #75A5FF;
76 | }
77 |
78 | .hljs-title {
79 | color: #75A5FF;
80 | }
81 |
82 | .hljs-section {
83 | color: #ffc66d;
84 | }
85 |
86 | .hljs-addition {
87 | background-color: #144212;
88 | color: #e6e1dc;
89 | display: inline-block;
90 | width: 100%;
91 | }
92 |
93 | .hljs-deletion {
94 | background-color: #600;
95 | color: #e6e1dc;
96 | display: inline-block;
97 | width: 100%;
98 | }
99 |
100 | .hljs-selector-class {
101 | color: #FFCB68;
102 | }
103 |
104 | .hljs-selector-id {
105 | color: #F77669;
106 | }
107 |
108 | .hljs-emphasis {
109 | font-style: italic;
110 | }
111 |
112 | .hljs-strong {
113 | font-weight: bold;
114 | }
115 |
116 | .hljs-link {
117 | text-decoration: underline;
118 | }
119 |
--------------------------------------------------------------------------------
/src/app/shared/material/material.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { MatButtonModule } from '@angular/material/button';
3 | import { MatCardModule } from '@angular/material/card';
4 | import { MatCheckboxModule } from '@angular/material/checkbox';
5 | import { MatNativeDateModule } from '@angular/material/core';
6 | import { MatDatepickerModule } from '@angular/material/datepicker';
7 | import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
8 | import { MatIconModule } from '@angular/material/icon';
9 | import { MatInputModule } from '@angular/material/input';
10 | import { MatListModule } from '@angular/material/list';
11 | import { MatRadioModule } from '@angular/material/radio';
12 | import { MatSelectModule } from '@angular/material/select';
13 | import { MatSidenavModule } from '@angular/material/sidenav';
14 | import { MatTabsModule } from '@angular/material/tabs';
15 |
16 | import { CustomErrorStateMatcherDirective } from './error-state-matcher';
17 | import { MatListOptionFixDirective } from './mat-list-option-fix';
18 | import { NgrxMatSelectViewAdapter } from './mat-select-view-adapter';
19 |
20 | @NgModule({
21 | imports: [
22 | MatInputModule,
23 | MatCheckboxModule,
24 | MatSelectModule,
25 | MatRadioModule,
26 | MatCardModule,
27 | MatButtonModule,
28 | MatSidenavModule,
29 | MatListModule,
30 | MatIconModule,
31 | MatTabsModule,
32 | MatDatepickerModule,
33 | MatNativeDateModule,
34 | ],
35 | declarations: [
36 | NgrxMatSelectViewAdapter,
37 | CustomErrorStateMatcherDirective,
38 | MatListOptionFixDirective,
39 | ],
40 | exports: [
41 | MatInputModule,
42 | MatCheckboxModule,
43 | MatSelectModule,
44 | MatRadioModule,
45 | MatCardModule,
46 | MatButtonModule,
47 | MatSidenavModule,
48 | MatListModule,
49 | MatIconModule,
50 | MatTabsModule,
51 | MatDatepickerModule,
52 | MatNativeDateModule,
53 | NgrxMatSelectViewAdapter,
54 | CustomErrorStateMatcherDirective,
55 | MatListOptionFixDirective,
56 | ],
57 | providers: [
58 | { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { float: 'always' } },
59 | ],
60 | })
61 | export class MaterialModule { }
62 |
--------------------------------------------------------------------------------
/src/assets/img/components/datepicker.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/hero.reducers.ts:
--------------------------------------------------------------------------------
1 | import {Action, createReducer, on} from '@ngrx/store';
2 | import {Hero} from '../../heroes/models';
3 | import * as fromHeroActions from './hero.actions';
4 | import {createEntityAdapter, EntityAdapter, EntityState} from '@ngrx/entity';
5 |
6 | export const HERO_FEATURE_KEY = 'heroes';
7 |
8 | export interface State extends EntityState {
9 | loaded: boolean;
10 | error?: Error | any;
11 | }
12 |
13 | export const adapter: EntityAdapter = createEntityAdapter();
14 |
15 | export interface HeroPartialState {
16 | readonly [HERO_FEATURE_KEY]: State;
17 | }
18 |
19 | export const initialState: State = adapter.getInitialState({
20 | // Additional entity state properties
21 | loaded: false,
22 | error: null
23 | });
24 |
25 | const _reducer = createReducer(
26 | initialState,
27 | on(fromHeroActions.loadHeroesSuccess, (state, {data}) => {
28 | return adapter.setAll(data, {
29 | ...state,
30 | loaded: true
31 | });
32 | }),
33 | on(fromHeroActions.loadHeroesFail, (state, {error}) => {
34 | return {
35 | ...state,
36 | error
37 | };
38 | }),
39 |
40 | on(fromHeroActions.loadHeroSuccess, (state, {entity}) => {
41 | return adapter.setOne(entity, state);
42 | }),
43 | on(fromHeroActions.loadHeroesFail, (state, {error}) => {
44 | return {
45 | ...state,
46 | error
47 | };
48 | }),
49 |
50 | on(fromHeroActions.createHeroSuccess, (state, {entity}) => {
51 | return adapter.addOne(entity, state);
52 | }),
53 | on(fromHeroActions.createHeroFail, (state, {error}) => {
54 | return {
55 | ...state,
56 | error
57 | };
58 | }),
59 |
60 | on(fromHeroActions.updateHeroSuccess, (state, {entity}) => {
61 | return adapter.updateOne({id: entity.id, changes: entity}, state);
62 | }),
63 | on(fromHeroActions.updateHeroFail, (state, {error}) => {
64 | return {
65 | ...state,
66 | error
67 | };
68 | }),
69 |
70 | on(fromHeroActions.deleteHeroSuccess, (state, {id}) => {
71 | return adapter.removeOne(id, state);
72 | }),
73 | on(fromHeroActions.deleteHeroFail, (state, {error}) => {
74 | return {
75 | ...state,
76 | error
77 | };
78 | }),
79 | );
80 |
81 | export function reducer(state: State | undefined, action: Action) {
82 | return _reducer(state, action);
83 | }
84 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "array-type": false,
5 | "arrow-parens": false,
6 | "deprecation": {
7 | "severity": "warn"
8 | },
9 | "component-class-suffix": true,
10 | "contextual-lifecycle": true,
11 | "directive-class-suffix": true,
12 | "directive-selector": [
13 | true,
14 | "attribute",
15 | "app",
16 | "camelCase"
17 | ],
18 | "component-selector": [
19 | true,
20 | "element",
21 | "app",
22 | "kebab-case"
23 | ],
24 | "import-blacklist": [
25 | true,
26 | "rxjs/Rx"
27 | ],
28 | "interface-name": false,
29 | "max-classes-per-file": false,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-consecutive-blank-lines": false,
47 | "no-console": [
48 | true,
49 | "debug",
50 | "info",
51 | "time",
52 | "timeEnd",
53 | "trace"
54 | ],
55 | "no-empty": false,
56 | "no-inferrable-types": [
57 | true,
58 | "ignore-params"
59 | ],
60 | "no-non-null-assertion": true,
61 | "no-redundant-jsdoc": true,
62 | "no-switch-case-fall-through": true,
63 | "no-use-before-declare": true,
64 | "no-var-requires": false,
65 | "object-literal-key-quotes": [
66 | true,
67 | "as-needed"
68 | ],
69 | "object-literal-sort-keys": false,
70 | "ordered-imports": false,
71 | "quotemark": [
72 | true,
73 | "single"
74 | ],
75 | "trailing-comma": false,
76 | "no-conflicting-lifecycle": true,
77 | "no-host-metadata-property": true,
78 | "no-input-rename": true,
79 | "no-inputs-metadata-property": true,
80 | "no-output-native": true,
81 | "no-output-on-prefix": true,
82 | "no-output-rename": true,
83 | "no-outputs-metadata-property": true,
84 | "template-banana-in-box": true,
85 | "template-no-negated-async": true,
86 | "use-lifecycle-interface": true,
87 | "use-pipe-transform-interface": true
88 | },
89 | "rulesDirectory": [
90 | "codelyzer"
91 | ]
92 | }
--------------------------------------------------------------------------------
/src/app/shared/material/mat-select-view-adapter.ts:
--------------------------------------------------------------------------------
1 | import { AfterViewInit, Directive, forwardRef, OnDestroy } from '@angular/core';
2 | import { MatSelect } from '@angular/material/select';
3 | import { FormViewAdapter, NGRX_FORM_VIEW_ADAPTER } from 'ngrx-forms';
4 | import { Subscription } from 'rxjs';
5 |
6 | // tslint:disable:directive-selector
7 | // tslint:disable:directive-class-suffix
8 | // necessary since material 2 does not properly export the mat-select as a NG_VALUE_ACCESSOR
9 | @Directive({
10 | selector: 'mat-select[ngrxFormControlState]',
11 | providers: [{
12 | provide: NGRX_FORM_VIEW_ADAPTER,
13 | useExisting: forwardRef(() => NgrxMatSelectViewAdapter),
14 | multi: true,
15 | }],
16 | })
17 | export class NgrxMatSelectViewAdapter implements FormViewAdapter, AfterViewInit, OnDestroy {
18 | private value: any;
19 | private subscriptions: Subscription[] = [];
20 |
21 | constructor(private matSelect: MatSelect) {}
22 |
23 | ngAfterViewInit() {
24 | this.subscriptions.push(
25 | this.matSelect.options.changes.subscribe(() => {
26 | Promise.resolve().then(() => this.matSelect.writeValue(this.value));
27 | })
28 | );
29 | }
30 |
31 | ngOnDestroy() {
32 | this.subscriptions.forEach(s => s.unsubscribe());
33 | }
34 |
35 | setViewValue(value: any) {
36 | this.value = value;
37 |
38 | // we have to verify that the same value is not set again since that would
39 | // cause focus to get lost on the select since it tries to focus the active option
40 | const selectedOption = this.matSelect.selected;
41 |
42 | if (selectedOption) {
43 | if (Array.isArray(selectedOption) && Array.isArray(value)) {
44 | if (value.length === selectedOption.length && value.every((v, i) => v === selectedOption[i])) {
45 | return;
46 | }
47 | } else if (!Array.isArray(selectedOption)) {
48 | if (value === selectedOption.value) {
49 | return;
50 | }
51 | }
52 | }
53 |
54 | // because the options are potentially updated AFTER the value (because of their order in the DOM),
55 | // setting the value has to be deferred, otherwise we might select an option which is not available yet.
56 | Promise.resolve().then(() => this.matSelect.writeValue(value));
57 | }
58 |
59 | setOnChangeCallback(fn: any) {
60 | this.matSelect.registerOnChange(value => {
61 | this.value = value;
62 | fn(value);
63 | });
64 | }
65 |
66 | setOnTouchedCallback(fn: any) {
67 | this.matSelect.registerOnTouched(fn);
68 | }
69 |
70 | setIsDisabled(isDisabled: boolean) {
71 | this.matSelect.setDisabledState(isDisabled);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/fastandconsistent.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/versatile.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/heroes/hero-form/hero-form.component.html:
--------------------------------------------------------------------------------
1 |
2 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
22 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
23 |
24 | /**
25 | * Web Animations `@angular/platform-browser/animations`
26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
28 | */
29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
30 |
31 | /**
32 | * By default, zone.js will patch all possible macroTask and DomEvents
33 | * user can disable parts of macroTask/DomEvents patch by setting following flags
34 | * because those flags need to be set before `zone.js` being loaded, and webpack
35 | * will put import in the top of bundle, so user need to create a separate file
36 | * in this directory (for example: zone-flags.ts), and put the following flags
37 | * into that file, and then add the following code before importing zone.js.
38 | * import './zone-flags.ts';
39 | *
40 | * The flags allowed in zone-flags.ts are listed here.
41 | *
42 | * The following flags will work for all browsers.
43 | *
44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
47 | *
48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
50 | *
51 | * (window as any).__Zone_enable_cross_context_check = true;
52 | *
53 | */
54 |
55 | /***************************************************************************************************
56 | * Zone JS is required by default for Angular itself.
57 | */
58 | import 'zone.js/dist/zone'; // Included with Angular CLI.
59 |
60 |
61 | /***************************************************************************************************
62 | * APPLICATION IMPORTS
63 | */
64 |
--------------------------------------------------------------------------------
/src/app/shared/theme-picker/theme-picker.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChangeDetectionStrategy,
3 | Component,
4 | OnDestroy,
5 | OnInit,
6 | ViewEncapsulation,
7 | } from '@angular/core';
8 | import {MatIconRegistry} from '@angular/material/icon';
9 | import {ActivatedRoute, ParamMap} from '@angular/router';
10 | import {Subscription} from 'rxjs';
11 | import {map} from 'rxjs/operators';
12 | import {DocsSiteTheme, ThemeService} from '../../services/theme.service';
13 | import {LiveAnnouncer} from '@angular/cdk/a11y';
14 | import {DomSanitizer} from '@angular/platform-browser';
15 | import {StyleService} from '../../services/style.service';
16 |
17 | @Component({
18 | selector: 'app-theme-picker',
19 | templateUrl: 'theme-picker.component.html',
20 | styleUrls: ['theme-picker.component.scss'],
21 | changeDetection: ChangeDetectionStrategy.OnPush,
22 | encapsulation: ViewEncapsulation.None
23 | })
24 | export class ThemePickerComponent implements OnInit, OnDestroy {
25 | private _queryParamSubscription = Subscription.EMPTY;
26 | currentTheme: DocsSiteTheme;
27 |
28 | // The below colors need to align with the themes defined in theme-picker.scss
29 | themes: DocsSiteTheme[] = [
30 | {
31 | primary: '#673AB7',
32 | accent: '#FFC107',
33 | displayName: 'Deep Purple & Amber',
34 | name: 'deeppurple-amber',
35 | isDark: false,
36 | },
37 | {
38 | primary: '#3F51B5',
39 | accent: '#E91E63',
40 | displayName: 'Indigo & Pink',
41 | name: 'indigo-pink',
42 | isDark: false,
43 | isDefault: true,
44 | },
45 | {
46 | primary: '#E91E63',
47 | accent: '#607D8B',
48 | displayName: 'Pink & Blue-grey',
49 | name: 'pink-bluegrey',
50 | isDark: true,
51 | },
52 | {
53 | primary: '#9C27B0',
54 | accent: '#4CAF50',
55 | displayName: 'Purple & Green',
56 | name: 'purple-green',
57 | isDark: true,
58 | },
59 | ];
60 |
61 | constructor(public styleManager: StyleService,
62 | private _themeStorage: ThemeService,
63 | private _activatedRoute: ActivatedRoute,
64 | private liveAnnouncer: LiveAnnouncer,
65 | iconRegistry: MatIconRegistry,
66 | sanitizer: DomSanitizer) {
67 | iconRegistry.addSvgIcon('theme-example',
68 | sanitizer.bypassSecurityTrustResourceUrl(
69 | 'assets/img/theme-demo-icon.svg'));
70 | const themeName = this._themeStorage.getStoredThemeName();
71 | if (themeName) {
72 | this.selectTheme(themeName);
73 | }
74 | }
75 |
76 | ngOnInit() {
77 | this._queryParamSubscription = this._activatedRoute.queryParamMap
78 | .pipe(map((params: ParamMap) => params.get('theme')))
79 | .subscribe((themeName: string | null) => {
80 | if (themeName) {
81 | this.selectTheme(themeName);
82 | }
83 | });
84 | }
85 |
86 | ngOnDestroy() {
87 | this._queryParamSubscription.unsubscribe();
88 | }
89 |
90 | selectTheme(themeName: string) {
91 | const theme = this.themes.find(currentTheme => currentTheme.name === themeName);
92 |
93 | if (!theme) {
94 | return;
95 | }
96 |
97 | this.currentTheme = theme;
98 |
99 | if (theme.isDefault) {
100 | this.styleManager.removeStyle('theme');
101 | } else {
102 | this.styleManager.setStyle('theme', `assets/${theme.name}.css`);
103 | }
104 |
105 | if (this.currentTheme) {
106 | this.liveAnnouncer.announce(`${theme.displayName} theme selected.`, 'polite', 3000).then();
107 | this._themeStorage.storeTheme(this.currentTheme);
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/app/root-store/hero-store/hero.effects.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Actions, createEffect, ofType} from '@ngrx/effects';
3 | import {catchError, map, switchMap, tap} from 'rxjs/operators';
4 | import {of} from 'rxjs';
5 | import {HeroesService} from '../../services/heroes.service';
6 | import * as fromHeroActions from './hero.actions';
7 | import {Router} from '@angular/router';
8 |
9 | @Injectable()
10 | export class HeroEffects {
11 |
12 | loadHeroes$ = createEffect(() =>
13 | this.actions$.pipe(
14 | ofType(fromHeroActions.loadHeroes),
15 | switchMap(() =>
16 | this.heroesService.getAll().pipe(
17 | map((response: any) => {
18 | return fromHeroActions.loadHeroesSuccess({
19 | data: response
20 | });
21 | }),
22 | catchError(error => {
23 | return of(
24 | fromHeroActions.loadHeroesFail({
25 | error
26 | })
27 | );
28 | })
29 | )
30 | )
31 | )
32 | );
33 |
34 | loadHero$ = createEffect(() =>
35 | this.actions$.pipe(
36 | ofType(fromHeroActions.loadHero),
37 | switchMap(({id}) =>
38 | this.heroesService.get(id).pipe(
39 | map((response: any) => {
40 | return fromHeroActions.loadHeroSuccess({
41 | entity: response
42 | });
43 | }),
44 | catchError(error => {
45 | return of(
46 | fromHeroActions.loadHeroFail({
47 | error
48 | })
49 | );
50 | })
51 | )
52 | )
53 | )
54 | );
55 |
56 | updateHero$ = createEffect(() =>
57 | this.actions$.pipe(
58 | ofType(fromHeroActions.updateHero),
59 | switchMap(action =>
60 | this.heroesService.update(action.entity.id, action.entity).pipe(
61 | map((response: any) => {
62 | return fromHeroActions.updateHeroSuccess({
63 | entity: response
64 | });
65 | }),
66 | catchError(error => {
67 | return of(
68 | fromHeroActions.updateHeroFail({
69 | error
70 | })
71 | );
72 | })
73 | )
74 | )
75 | )
76 | );
77 |
78 | createHero$ = createEffect(() =>
79 | this.actions$.pipe(
80 | ofType(fromHeroActions.createHero),
81 | switchMap(action =>
82 | this.heroesService.create(action.entity).pipe(
83 | map((response: any) => {
84 | return fromHeroActions.createHeroSuccess({
85 | entity: response
86 | });
87 | }),
88 | catchError(error => {
89 | return of(
90 | fromHeroActions.createHeroFail({
91 | error
92 | })
93 | );
94 | })
95 | )
96 | )
97 | )
98 | );
99 |
100 | deleteHero$ = createEffect(() =>
101 | this.actions$.pipe(
102 | ofType(fromHeroActions.deleteHero),
103 | switchMap(action =>
104 | this.heroesService.delete(action.id).pipe(
105 | map((response: any) => {
106 | return fromHeroActions.deleteHeroSuccess({
107 | id: response.id
108 | });
109 | }),
110 | catchError((error: Error) => {
111 | return of(
112 | fromHeroActions.deleteHeroFail({
113 | error
114 | })
115 | );
116 | })
117 | )
118 | )
119 | )
120 | );
121 |
122 | navigateTo$ = createEffect(
123 | () =>
124 | this.actions$.pipe(
125 | ofType(fromHeroActions.navigateTo),
126 | tap((action) => this.router.navigate([action.prop]))
127 | ),
128 | {dispatch: false}
129 | );
130 |
131 | constructor(
132 | private actions$: Actions,
133 | private heroesService: HeroesService,
134 | private router: Router
135 | ) {
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/sprintzerotoapp.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/optimized.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/img/homepage/material_splash.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "tour-of-heroes": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:browser",
19 | "options": {
20 | "outputPath": "dist/tour-of-heroes",
21 | "index": "src/index.html",
22 | "main": "src/main.ts",
23 | "polyfills": "src/polyfills.ts",
24 | "tsConfig": "tsconfig.app.json",
25 | "aot": true,
26 | "sourceMap": true,
27 | "assets": [
28 | {
29 | "glob": "**/*",
30 | "input": "src/assets",
31 | "output": "/assets"
32 | },
33 | {
34 | "glob": "favicon.ico",
35 | "input": "src",
36 | "output": "/"
37 | }
38 | ],
39 | "styles": [
40 | {
41 | "input": "src/styles.scss"
42 | },
43 | {
44 | "input": "src/highlightjs/material-light.scss"
45 | },
46 | {
47 | "inject": false,
48 | "input": "src/styles/custom-themes/pink-bluegrey.scss",
49 | "bundleName": "assets/pink-bluegrey"
50 | },
51 | {
52 | "inject": false,
53 | "input": "src/styles/custom-themes/deeppurple-amber.scss",
54 | "bundleName": "assets/deeppurple-amber"
55 | },
56 | {
57 | "inject": false,
58 | "input": "src/styles/custom-themes/indigo-pink.scss",
59 | "bundleName": "assets/indigo-pink"
60 | },
61 | {
62 | "inject": false,
63 | "input": "src/styles/custom-themes/purple-green.scss",
64 | "bundleName": "assets/purple-green"
65 | }
66 | ],
67 | "stylePreprocessorOptions": {
68 | "includePaths": ["node_modules", "src", "src/styles"]
69 | },
70 | "scripts": [],
71 | "allowedCommonJsDependencies": [
72 | "lodash"
73 | ]
74 | },
75 | "configurations": {
76 | "production": {
77 | "fileReplacements": [
78 | {
79 | "replace": "src/environments/environment.ts",
80 | "with": "src/environments/environment.prod.ts"
81 | }
82 | ],
83 | "optimization": true,
84 | "outputHashing": "all",
85 | "sourceMap": false,
86 | "namedChunks": false,
87 | "aot": true,
88 | "extractLicenses": true,
89 | "vendorChunk": false,
90 | "buildOptimizer": true,
91 | "budgets": [
92 | {
93 | "type": "initial",
94 | "maximumWarning": "500kb",
95 | "maximumError": "1mb"
96 | },
97 | {
98 | "type": "anyComponentStyle",
99 | "maximumWarning": "6kb",
100 | "maximumError": "6kb"
101 | }
102 | ]
103 | },
104 | "development": {
105 | "fileReplacements": [
106 | {
107 | "replace": "src/environments/environment.ts",
108 | "with": "src/environments/environment.dev.ts"
109 | }
110 | ],
111 | "optimization": true,
112 | "outputHashing": "all",
113 | "sourceMap": false,
114 | "namedChunks": false,
115 | "aot": true,
116 | "extractLicenses": true,
117 | "vendorChunk": false,
118 | "buildOptimizer": true,
119 | "budgets": [
120 | {
121 | "type": "initial",
122 | "maximumWarning": "500kb",
123 | "maximumError": "1mb"
124 | },
125 | {
126 | "type": "anyComponentStyle",
127 | "maximumWarning": "2kb",
128 | "maximumError": "4kb"
129 | }
130 | ]
131 | }
132 | }
133 | },
134 | "serve": {
135 | "builder": "@angular-devkit/build-angular:dev-server",
136 | "options": {
137 | "browserTarget": "tour-of-heroes:build"
138 | },
139 | "configurations": {
140 | "production": {
141 | "browserTarget": "tour-of-heroes:build:production"
142 | }
143 | }
144 | },
145 | "extract-i18n": {
146 | "builder": "@angular-devkit/build-angular:extract-i18n",
147 | "options": {
148 | "browserTarget": "tour-of-heroes:build"
149 | }
150 | },
151 | "test": {
152 | "builder": "@angular-devkit/build-angular:karma",
153 | "options": {
154 | "main": "src/test.ts",
155 | "polyfills": "src/polyfills.ts",
156 | "tsConfig": "tsconfig.spec.json",
157 | "karmaConfig": "karma.conf.js",
158 | "assets": [
159 | {
160 | "glob": "**/*",
161 | "input": "src/assets",
162 | "output": "/assets"
163 | },
164 | {
165 | "glob": "favicon.ico",
166 | "input": "src",
167 | "output": "/"
168 | }
169 | ],
170 | "styles": [
171 | {
172 | "input": "src/styles.scss"
173 | },
174 | {
175 | "input": "src/highlightjs/material-light.scss"
176 | },
177 | {
178 | "inject": false,
179 | "input": "src/styles/custom-themes/pink-bluegrey.scss",
180 | "bundleName": "assets/pink-bluegrey"
181 | },
182 | {
183 | "inject": false,
184 | "input": "src/styles/custom-themes/deeppurple-amber.scss",
185 | "bundleName": "assets/deeppurple-amber"
186 | },
187 | {
188 | "inject": false,
189 | "input": "src/styles/custom-themes/indigo-pink.scss",
190 | "bundleName": "assets/indigo-pink"
191 | },
192 | {
193 | "inject": false,
194 | "input": "src/styles/custom-themes/purple-green.scss",
195 | "bundleName": "assets/purple-green"
196 | }
197 | ],
198 | "stylePreprocessorOptions": {
199 | "includePaths": ["node_modules", "src", "src/styles"]
200 | },
201 | "scripts": []
202 | }
203 | },
204 | "lint": {
205 | "builder": "@angular-devkit/build-angular:tslint",
206 | "options": {
207 | "tsConfig": [
208 | "tsconfig.app.json",
209 | "tsconfig.spec.json",
210 | "e2e/tsconfig.json"
211 | ],
212 | "exclude": [
213 | "**/node_modules/**"
214 | ]
215 | }
216 | },
217 | "e2e": {
218 | "builder": "@angular-devkit/build-angular:protractor",
219 | "options": {
220 | "protractorConfig": "e2e/protractor.conf.js",
221 | "devServerTarget": "tour-of-heroes:serve"
222 | },
223 | "configurations": {
224 | "production": {
225 | "devServerTarget": "tour-of-heroes:serve:production"
226 | }
227 | }
228 | }
229 | }
230 | }},
231 | "defaultProject": "tour-of-heroes"
232 | }
233 |
--------------------------------------------------------------------------------