├── .angular-cli.json
├── .editorconfig
├── .gitignore
├── .sass-lint.yml
├── README.md
├── circle.yml
├── data
├── platforms.json
└── video-games.json
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── karma.conf.js
├── package.json
├── protractor.conf.js
├── src
├── app
│ ├── interfaces
│ │ └── app-state.interface.ts
│ ├── loading
│ │ ├── components
│ │ │ ├── index.ts
│ │ │ ├── loading-container
│ │ │ │ ├── index.ts
│ │ │ │ ├── loading-container.component.html
│ │ │ │ ├── loading-container.component.scss
│ │ │ │ └── loading-container.component.ts
│ │ │ └── spinner
│ │ │ │ ├── index.ts
│ │ │ │ ├── spinner.component.html
│ │ │ │ ├── spinner.component.scss
│ │ │ │ └── spinner.component.ts
│ │ ├── interfaces
│ │ │ ├── index.ts
│ │ │ └── loadable
│ │ │ │ ├── index.ts
│ │ │ │ └── loadable.ts
│ │ └── loading.module.ts
│ ├── root
│ │ ├── components
│ │ │ ├── index.ts
│ │ │ └── root
│ │ │ │ ├── index.ts
│ │ │ │ ├── root.component.html
│ │ │ │ ├── root.component.scss
│ │ │ │ ├── root.component.spec.ts
│ │ │ │ └── root.component.ts
│ │ ├── root.module.ts
│ │ ├── root.routes.ts
│ │ └── services
│ │ │ ├── api
│ │ │ ├── api.service.ts
│ │ │ └── index.ts
│ │ │ └── index.ts
│ ├── store
│ │ ├── create-action.ts
│ │ ├── reducer-helpers.ts
│ │ └── root-reducer.ts
│ ├── test
│ │ ├── configure-test-module.function.ts
│ │ └── test-component-support.class.ts
│ └── video-games
│ │ ├── components
│ │ ├── index.ts
│ │ └── video-games-container
│ │ │ ├── index.ts
│ │ │ ├── video-games-container.component.html
│ │ │ ├── video-games-container.component.scss
│ │ │ └── video-games-container.component.ts
│ │ ├── detail
│ │ ├── components
│ │ │ ├── button-group
│ │ │ │ ├── button-group.component.html
│ │ │ │ ├── button-group.component.scss
│ │ │ │ ├── button-group.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── button-toggle
│ │ │ │ ├── button-toggle.component.html
│ │ │ │ ├── button-toggle.component.scss
│ │ │ │ ├── button-toggle.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── button
│ │ │ │ ├── button.component.html
│ │ │ │ ├── button.component.scss
│ │ │ │ ├── button.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── video-game-detail-page
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-detail-page.component.html
│ │ │ │ ├── video-game-detail-page.component.scss
│ │ │ │ └── video-game-detail-page.component.ts
│ │ │ ├── video-game-detail
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-detail.component.html
│ │ │ │ ├── video-game-detail.component.scss
│ │ │ │ └── video-game-detail.component.ts
│ │ │ └── youtube-video
│ │ │ │ ├── index.ts
│ │ │ │ ├── youtube-video.component.html
│ │ │ │ ├── youtube-video.component.scss
│ │ │ │ └── youtube-video.component.ts
│ │ ├── detail.module.ts
│ │ └── detail.routes.ts
│ │ ├── interfaces
│ │ ├── index.ts
│ │ ├── platforms
│ │ │ ├── index.ts
│ │ │ └── platforms.interface.ts
│ │ ├── video-game-listing
│ │ │ ├── index.ts
│ │ │ ├── video-game-filters.functions.ts
│ │ │ ├── video-game-filters.interface.ts
│ │ │ ├── video-game-listing.functions.spec.ts
│ │ │ ├── video-game-listing.functions.ts
│ │ │ └── video-game-listing.interface.ts
│ │ └── video-game
│ │ │ ├── index.ts
│ │ │ ├── video-game.functions.spec.ts
│ │ │ ├── video-game.functions.ts
│ │ │ └── video-game.interface.ts
│ │ ├── listing
│ │ ├── components
│ │ │ ├── card
│ │ │ │ ├── card.component.html
│ │ │ │ ├── card.component.scss
│ │ │ │ ├── card.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── cards
│ │ │ │ ├── cards.component.html
│ │ │ │ ├── cards.component.scss
│ │ │ │ ├── cards.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── favorite-toggle
│ │ │ │ ├── favorite-toggle.component.html
│ │ │ │ ├── favorite-toggle.component.scss
│ │ │ │ ├── favorite-toggle.component.ts
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── video-game-filters
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-filters.component.html
│ │ │ │ ├── video-game-filters.component.scss
│ │ │ │ └── video-game-filters.component.ts
│ │ │ ├── video-game-list-item
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-list-item.component.html
│ │ │ │ ├── video-game-list-item.component.scss
│ │ │ │ └── video-game-list-item.component.ts
│ │ │ ├── video-game-listing-page
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-listing-page.component.html
│ │ │ │ ├── video-game-listing-page.component.scss
│ │ │ │ └── video-game-listing-page.component.ts
│ │ │ ├── video-game-listing
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-listing.component.html
│ │ │ │ ├── video-game-listing.component.scss
│ │ │ │ └── video-game-listing.component.ts
│ │ │ └── video-game-search
│ │ │ │ ├── index.ts
│ │ │ │ ├── video-game-search.component.html
│ │ │ │ ├── video-game-search.component.scss
│ │ │ │ └── video-game-search.component.ts
│ │ ├── listing.module.ts
│ │ └── listing.routes.ts
│ │ ├── services
│ │ ├── index.ts
│ │ ├── platforms.service.ts
│ │ └── video-games.service.ts
│ │ ├── store
│ │ ├── effects.ts
│ │ ├── platforms
│ │ │ ├── platforms.effects.ts
│ │ │ ├── platforms.reducer.spec.ts
│ │ │ ├── platforms.reducer.ts
│ │ │ └── platforms.store.ts
│ │ ├── reducers.ts
│ │ ├── stores.ts
│ │ └── video-game-listing
│ │ │ ├── video-game-listing.effects.ts
│ │ │ ├── video-game-listing.reducer.spec.ts
│ │ │ ├── video-game-listing.reducer.ts
│ │ │ └── video-game-listing.store.ts
│ │ ├── video-games.module.ts
│ │ └── video-games.routes.ts
├── assets
│ └── .gitkeep
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.css
├── test.ts
├── tsconfig.app.json
└── tsconfig.spec.json
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.angular-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "project": {
4 | "name": "angular2-ngrx-example"
5 | },
6 | "apps": [
7 | {
8 | "root": "src",
9 | "outDir": "dist",
10 | "assets": [
11 | "assets",
12 | "favicon.ico"
13 | ],
14 | "index": "index.html",
15 | "main": "main.ts",
16 | "polyfills": "polyfills.ts",
17 | "test": "test.ts",
18 | "tsconfig": "tsconfig.app.json",
19 | "testTsconfig": "tsconfig.spec.json",
20 | "prefix": "app",
21 | "styles": [
22 | "styles.css",
23 | "../node_modules/font-awesome/css/font-awesome.css"
24 | ],
25 | "scripts": [],
26 | "environmentSource": "environments/environment.ts",
27 | "environments": {
28 | "dev": "environments/environment.ts",
29 | "prod": "environments/environment.prod.ts"
30 | }
31 | }
32 | ],
33 | "addons": ["../node_modules/font-awesome/fonts/*.+(otf|eot|svg|ttf|woff|woff2)"],
34 | "e2e": {
35 | "protractor": {
36 | "config": "./protractor.conf.js"
37 | }
38 | },
39 | "lint": [
40 | {
41 | "project": "src/tsconfig.app.json"
42 | },
43 | {
44 | "project": "src/tsconfig.spec.json"
45 | },
46 | {
47 | "project": "e2e/tsconfig.e2e.json"
48 | }
49 | ],
50 | "test": {
51 | "karma": {
52 | "config": "./karma.conf.js"
53 | }
54 | },
55 | "defaults": {
56 | "styleExt": "css",
57 | "component": {}
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://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 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 |
10 | # IDEs and editors
11 | /.idea
12 | .project
13 | .classpath
14 | .c9/
15 | *.launch
16 | .settings/
17 |
18 | # IDE - VSCode
19 | .vscode/
20 | !.vscode/settings.json
21 | !.vscode/tasks.json
22 | !.vscode/launch.json
23 | !.vscode/extensions.json
24 |
25 | # misc
26 | /.sass-cache
27 | /connect.lock
28 | coverage/
29 | /libpeerconnection.log
30 | npm-debug.log
31 | testem.log
32 | /typings
33 |
34 | # e2e
35 | /e2e/*.js
36 | /e2e/*.map
37 |
38 | #System Files
39 | .DS_Store
40 | Thumbs.db
41 |
--------------------------------------------------------------------------------
/.sass-lint.yml:
--------------------------------------------------------------------------------
1 | options:
2 | formatter: stylish
3 | files:
4 | include: 'src/**/*.s+(a|c)ss'
5 | rules:
6 | # Extends
7 | extends-before-mixins: 1
8 | extends-before-declarations: 1
9 | placeholder-in-extend: 1
10 |
11 | # Mixins
12 | mixins-before-declarations: 1
13 |
14 | # Line Spacing
15 | one-declaration-per-line: 1
16 | empty-line-between-blocks: 1
17 | single-line-per-selector: 1
18 |
19 | # Disallows
20 | no-attribute-selectors: 0
21 | no-color-hex: 0
22 | no-color-keywords: 1
23 | no-color-literals: 1
24 | no-combinators: 0
25 | no-css-comments: 1
26 | no-debug: 1
27 | no-disallowed-properties: 0
28 | no-duplicate-properties: 1
29 | no-empty-rulesets: 1
30 | no-extends: 0
31 | no-ids: 1
32 | no-important: 1
33 | no-invalid-hex: 1
34 | no-mergeable-selectors: 1
35 | no-misspelled-properties: 1
36 | no-qualifying-elements: 1
37 | no-trailing-whitespace: 1
38 | no-trailing-zero: 1
39 | no-transition-all: 1
40 | no-universal-selectors: 0
41 | no-url-domains: 1
42 | no-url-protocols: 1
43 | no-vendor-prefixes: 1
44 | no-warn: 1
45 | property-units: 0
46 |
47 | # Nesting
48 | declarations-before-nesting: 1
49 | force-attribute-nesting: 1
50 | force-element-nesting: 1
51 | force-pseudo-nesting: 1
52 |
53 | # Name Formats
54 | class-name-format: 1
55 | function-name-format: 1
56 | id-name-format: 0
57 | mixin-name-format: 1
58 | placeholder-name-format: 1
59 | variable-name-format: 1
60 |
61 | # Style Guide
62 | attribute-quotes: 1
63 | bem-depth: 0
64 | border-zero: 1
65 | brace-style: 1
66 | clean-import-paths: 1
67 | empty-args: 1
68 | hex-length: 1
69 | hex-notation: 1
70 | indentation: 1
71 | leading-zero: 1
72 | max-line-length: 0
73 | max-file-line-count: 0
74 | nesting-depth: 1
75 | property-sort-order: 1
76 | pseudo-element: 1
77 | quotes: 1
78 | shorthand-values: 1
79 | url-quotes: 1
80 | variable-for-property: 1
81 | zero-unit: 1
82 |
83 | # Inner Spacing
84 | space-after-comma: 1
85 | space-before-colon: 1
86 | space-after-colon: 1
87 | space-before-brace: 1
88 | space-before-bang: 1
89 | space-after-bang: 1
90 | space-between-parens: 1
91 | space-around-operator: 1
92 |
93 | # Final Items
94 | trailing-semicolon: 1
95 | final-newline: 1
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular 2 Ngrx Example
2 |
3 | This project was generated with [angular-cli](https://github.com/angular/angular-cli) version 1.0.0-beta.25.5.
4 |
5 | ## Development server
6 | 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.
7 |
8 | ## Code scaffolding
9 |
10 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
11 |
12 | ## Build
13 |
14 | 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.
15 |
16 | ## Running unit tests
17 |
18 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
19 |
20 | ## Running end-to-end tests
21 |
22 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
23 | Before running the tests make sure you are serving the app via `ng serve`.
24 |
25 | ## Deploying to GitHub Pages
26 |
27 | Run `ng github-pages:deploy` to deploy to GitHub Pages.
28 |
29 | ## Further help
30 |
31 | 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).
32 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 6.9.5
4 |
--------------------------------------------------------------------------------
/data/platforms.json:
--------------------------------------------------------------------------------
1 | [
2 | "Nintendo Switch",
3 | "Nintendo 3DS",
4 | "PC"
5 | ]
--------------------------------------------------------------------------------
/data/video-games.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "1",
4 | "title": "The Legend of Zelda: Breath of the Wild",
5 | "favorite": true,
6 | "platform": "Nintendo Switch",
7 | "description": "description",
8 | "youtubeUrl": "https://www.youtube.com/embed/zw47_q9wbBE",
9 | "imageUrl": "http://cdn.idigitaltimes.com/sites/idigitaltimes.com/files/2017/01/27/zelda-breath-wild.jpg"
10 | },
11 | {
12 | "id": "2",
13 | "title": "Super Mario Odyssey",
14 | "favorite": false,
15 | "platform": "Nintendo Switch",
16 | "description": "description",
17 | "youtubeUrl": "https://www.youtube.com/embed/5kcdRBHM7kM",
18 | "imageUrl": "http://cdn02.nintendo-europe.com/media/images/10_share_images/games_15/nintendo_switch_4/H2x1_NSwitch_SuperMarioOdyssey.jpg"
19 | },
20 | {
21 | "id": "3",
22 | "title": "Pillars of Eternity 2: Deadfire",
23 | "favorite": false,
24 | "platform": "PC",
25 | "description": "description",
26 | "youtubeUrl": "https://www.youtube.com/embed/ln_plWALAoI",
27 | "imageUrl": "https://www.pcgamesn.com/sites/default/files/pillars%20of%20eternity%202%20campaign.png"
28 | },
29 | {
30 | "id": "4",
31 | "title": "Fire Emblem Echoes: Shadows of Valentia",
32 | "favorite": false,
33 | "platform": "Nintendo 3DS",
34 | "description": "description",
35 | "youtubeUrl": "https://www.youtube.com/embed/LOnvfYnp2ww",
36 | "imageUrl": "https://serenesforest.net/wp-content/uploads/2017/01/echoes-boxart.png"
37 | }
38 | ]
39 |
--------------------------------------------------------------------------------
/e2e/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { VideoGameTrailersPage } from './app.po';
2 |
3 | describe('Root', () => {
4 | let page: VideoGameTrailersPage;
5 |
6 | beforeEach(() => {
7 | page = new VideoGameTrailersPage();
8 | });
9 |
10 | it('should display the header text', () => {
11 | page.navigateTo();
12 | expect(page.getParagraphText()).toEqual('Video Game Trailers');
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, element, by } from 'protractor';
2 |
3 | export class VideoGameTrailersPage {
4 |
5 | public navigateTo() {
6 | return browser.get('/');
7 | }
8 |
9 | public getParagraphText() {
10 | return element(by.css('app-root h1')).getText();
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016"
10 | ],
11 | "outDir": "../dist/out-tsc-e2e",
12 | "module": "commonjs",
13 | "target": "es6",
14 | "types":[
15 | "jasmine",
16 | "node"
17 | ]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular/cli'],
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/cli/plugins/karma')
14 | ],
15 | client:{
16 | clearContext: false // leave Jasmine Spec Runner output visible in browser
17 | },
18 | files: [
19 | { pattern: './src/test.ts', watched: false }
20 | ],
21 | preprocessors: {
22 | './src/test.ts': ['@angular/cli']
23 | },
24 | mime: {
25 | 'text/x-typescript': ['ts','tsx']
26 | },
27 | coverageIstanbulReporter: {
28 | reports: [ 'html', 'lcovonly' ],
29 | fixWebpackSourcePaths: true
30 | },
31 | angularCli: {
32 | environment: 'dev'
33 | },
34 | reporters: config.angularCli && config.angularCli.codeCoverage
35 | ? ['progress', 'coverage-istanbul']
36 | : ['progress', 'kjhtml'],
37 | port: 9876,
38 | colors: true,
39 | logLevel: config.LOG_INFO,
40 | autoWatch: true,
41 | browsers: ['Chrome'],
42 | singleRun: false
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular2-ngrx-example",
3 | "version": "1.0.0",
4 | "engines": {
5 | "node": "6.9.5"
6 | },
7 | "license": "MIT",
8 | "angular-cli": {},
9 | "scripts": {
10 | "ng": "ng",
11 | "start": "npm run lint && ng serve",
12 | "test": "npm run lint && ng test --watch=false",
13 | "test-watch": "npm run lint && ng test",
14 | "lint": "ng lint",
15 | "e2e": "ng e2e"
16 | },
17 | "private": true,
18 | "dependencies": {
19 | "@angular/common": "^4.1.3",
20 | "@angular/compiler": "^4.1.3",
21 | "@angular/core": "^4.1.3",
22 | "@angular/forms": "^4.1.3",
23 | "@angular/http": "^4.1.3",
24 | "@angular/platform-browser": "^4.1.3",
25 | "@angular/platform-browser-dynamic": "^4.1.3",
26 | "@angular/router": "^4.1.3",
27 | "@ngrx/core": "^1.2.0",
28 | "@ngrx/effects": "^2.0.0",
29 | "@ngrx/router-store": "^1.2.5",
30 | "@ngrx/store": "^2.2.1",
31 | "@ngrx/store-devtools": "^3.2.3",
32 | "core-js": "^2.4.1",
33 | "font-awesome": "^4.7.0",
34 | "ngrx-store-localstorage": "^0.1.5",
35 | "ngrx-store-logger": "^0.1.7",
36 | "rxjs": "^5.2.0",
37 | "ts-helpers": "^1.1.1",
38 | "zone.js": "^0.8.11"
39 | },
40 | "devDependencies": {
41 | "@angular/cli": "^1.0.4",
42 | "@angular/compiler-cli": "^4.1.3",
43 | "@types/jasmine": "^2.5.47",
44 | "@types/node": "^7.0.21",
45 | "codelyzer": "~2.0.0",
46 | "jasmine-core": "^2.6.2",
47 | "jasmine-spec-reporter": "^4.1.0",
48 | "karma": "^1.7.0",
49 | "karma-chrome-launcher": "^2.1.1",
50 | "karma-cli": "~1.0.1",
51 | "karma-coverage-istanbul-reporter": "^1.2.1",
52 | "karma-jasmine": "~1.1.0",
53 | "karma-jasmine-html-reporter": "^0.2.2",
54 | "protractor": "~5.1.0",
55 | "sass-lint": "^1.10.2",
56 | "ts-node": "^3.0.4",
57 | "tslint": "~4.4.2",
58 | "typescript": "^2.1.6"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | const { SpecReporter } = require('jasmine-spec-reporter');
5 |
6 | exports.config = {
7 | allScriptsTimeout: 11000,
8 | specs: [
9 | './e2e/**/*.e2e-spec.ts'
10 | ],
11 | capabilities: {
12 | 'browserName': 'chrome'
13 | },
14 | directConnect: true,
15 | baseUrl: 'http://localhost:4200/',
16 | framework: 'jasmine',
17 | jasmineNodeOpts: {
18 | showColors: true,
19 | defaultTimeoutInterval: 30000,
20 | print: function() {}
21 | },
22 | beforeLaunch: function() {
23 | require('ts-node').register({
24 | project: 'e2e/tsconfig.e2e.json'
25 | });
26 | },
27 | onPrepare() {
28 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/src/app/interfaces/app-state.interface.ts:
--------------------------------------------------------------------------------
1 | import {IPlatforms, IVideoGameListing} from '../video-games/interfaces';
2 |
3 | export interface IAppState {
4 | readonly platforms: IPlatforms;
5 | readonly videoGameListing: IVideoGameListing;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/loading/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loading-container';
2 | export * from './spinner';
3 |
--------------------------------------------------------------------------------
/src/app/loading/components/loading-container/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loading-container.component';
2 |
--------------------------------------------------------------------------------
/src/app/loading/components/loading-container/loading-container.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Whoops, something went wrong!
6 |
7 | {{loadable.loadingError}}
8 |
9 |
10 |
11 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/loading/components/loading-container/loading-container.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex: 0;
4 | flex-direction: column;
5 | flex-grow: 1;
6 | height: 100%;
7 |
8 | app-spinner,
9 | .okay {
10 | display: flex;
11 | flex-grow: 1;
12 | }
13 |
14 | app-spinner {
15 | flex-direction: row;
16 | }
17 |
18 | .okay {
19 | flex-direction: column;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/loading/components/loading-container/loading-container.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, OnChanges} from '@angular/core';
2 |
3 | import {ILoadable} from '../../interfaces/loadable/loadable';
4 |
5 | @Component({
6 | selector: 'app-loading-container',
7 | templateUrl: 'loading-container.component.html',
8 | styleUrls: ['loading-container.component.scss']
9 | })
10 | export class LoadingContainerComponent implements OnChanges {
11 |
12 | @Input()
13 | public loadable: ILoadable;
14 |
15 | public isLoading: boolean;
16 | public isError: boolean;
17 | public isOkay: boolean;
18 |
19 | public ngOnChanges() {
20 | this.isLoading = this.loadable.isLoading;
21 | this.isError = Boolean(this.loadable.loadingError);
22 | this.isOkay = !this.isLoading && !this.isError;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/loading/components/spinner/index.ts:
--------------------------------------------------------------------------------
1 | export * from './spinner.component';
2 |
--------------------------------------------------------------------------------
/src/app/loading/components/spinner/spinner.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/loading/components/spinner/spinner.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | margin: 100px auto;
3 | text-align: center;
4 | width: 70px;
5 |
6 | @keyframes bounceDelay {
7 | 0%,
8 | 80%,
9 | 100% {
10 | transform: scale(0);
11 | }
12 | 40% {
13 | transform: scale(1);
14 | }
15 | }
16 |
17 | > div {
18 | background-color: #333;
19 | border-radius: 100%;
20 | display: inline-block;
21 | height: 18px;
22 | width: 18px;
23 | animation: bounceDelay 1.4s infinite ease-in-out both;
24 | }
25 |
26 | > .bounce1 {
27 | animation-delay: -0.32s;
28 | }
29 |
30 | > .bounce2 {
31 | animation-delay: -0.16s;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/app/loading/components/spinner/spinner.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-spinner',
5 | templateUrl: './spinner.component.html',
6 | styleUrls: ['./spinner.component.scss']
7 | })
8 | export class SpinnerComponent {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/loading/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loadable';
2 |
--------------------------------------------------------------------------------
/src/app/loading/interfaces/loadable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loadable';
2 |
--------------------------------------------------------------------------------
/src/app/loading/interfaces/loadable/loadable.ts:
--------------------------------------------------------------------------------
1 | export interface ILoadable {
2 | isLoading: boolean;
3 | loadingError: any;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/loading/loading.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {RouterModule} from '@angular/router';
4 |
5 | import {
6 | LoadingContainerComponent,
7 | SpinnerComponent
8 | } from './components/index';
9 |
10 | const components = [
11 | LoadingContainerComponent,
12 | SpinnerComponent
13 | ];
14 |
15 | @NgModule({
16 | imports: [
17 | CommonModule,
18 | RouterModule
19 | ],
20 | declarations: [
21 | ...components
22 | ],
23 | exports: [
24 | ...components
25 | ]
26 | })
27 | export class LoadingModule { }
28 |
--------------------------------------------------------------------------------
/src/app/root/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './root';
2 |
--------------------------------------------------------------------------------
/src/app/root/components/root/index.ts:
--------------------------------------------------------------------------------
1 | export * from './root.component';
2 |
--------------------------------------------------------------------------------
/src/app/root/components/root/root.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/root/components/root/root.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rangle/angular-ngrx-example/992375cb7f0fe3dac6f864676e1c2799005a49d2/src/app/root/components/root/root.component.scss
--------------------------------------------------------------------------------
/src/app/root/components/root/root.component.spec.ts:
--------------------------------------------------------------------------------
1 | import {async} from '@angular/core/testing';
2 | import {RouterTestingModule} from '@angular/router/testing';
3 |
4 | import {configureTestModule} from '../../../test/configure-test-module.function';
5 | import {RootComponent} from './root.component';
6 | import {TestComponentSupport} from '../../../test/test-component-support.class';
7 |
8 | describe('RootComponent', () => {
9 | let support;
10 |
11 | beforeEach(configureTestModule({
12 | imports: [
13 | RouterTestingModule.withRoutes([])
14 | ],
15 | declarations: [
16 | RootComponent
17 | ],
18 | }));
19 |
20 | it('should create the app', async(() => {
21 | support = new TestComponentSupport(RootComponent);
22 | expect(support.component).toBeTruthy();
23 | }));
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/root/components/root/root.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: 'root.component.html',
6 | styleUrls: ['root.component.scss']
7 | })
8 | export class RootComponent {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/root/root.module.ts:
--------------------------------------------------------------------------------
1 | import {BrowserModule} from '@angular/platform-browser';
2 | import {NgModule} from '@angular/core';
3 | import {FormsModule} from '@angular/forms';
4 | import {HttpModule} from '@angular/http';
5 | import {RouterModule} from '@angular/router';
6 | import {StoreModule} from '@ngrx/store';
7 | import {StoreDevtoolsModule} from '@ngrx/store-devtools';
8 | import {RouterStoreModule} from '@ngrx/router-store';
9 |
10 | import {VideoGamesModule} from '../video-games/video-games.module';
11 |
12 | import {rootRoutes} from './root.routes';
13 | import {rootReducer} from '../store/root-reducer';
14 |
15 | import {RootComponent} from './components';
16 |
17 | import {PlatformsStore, VideoGameListingStore} from '../video-games/store/stores';
18 | import {ApiService} from './services';
19 |
20 | @NgModule({
21 | imports: [
22 | BrowserModule,
23 | FormsModule,
24 | HttpModule,
25 | RouterModule.forRoot(rootRoutes),
26 | StoreModule.provideStore(rootReducer),
27 | RouterStoreModule.connectRouter(),
28 | StoreDevtoolsModule.instrumentOnlyWithExtension(),
29 | VideoGamesModule
30 | ],
31 | declarations: [
32 | RootComponent
33 | ],
34 | providers: [
35 | ApiService,
36 | PlatformsStore,
37 | VideoGameListingStore
38 | ],
39 | bootstrap: [RootComponent]
40 | })
41 | export class RootModule {
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/root/root.routes.ts:
--------------------------------------------------------------------------------
1 | import {Routes} from '@angular/router';
2 |
3 | import {videoGamesRoutes} from '../video-games/video-games.routes';
4 |
5 | export const rootRoutes: Routes = [
6 | ...videoGamesRoutes
7 | ];
8 |
--------------------------------------------------------------------------------
/src/app/root/services/api/api.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Http} from '@angular/http';
3 | import {Observable} from 'rxjs/Observable';
4 |
5 | @Injectable()
6 | export class ApiService {
7 |
8 | private readonly location = 'http://www.mocky.io/v2';
9 |
10 | constructor(private http: Http) {
11 |
12 | }
13 |
14 | public get(path: string): Observable {
15 | return this.http.get(`${this.location}${path}`)
16 | .map(response => response.json());
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/root/services/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from './api.service';
2 |
--------------------------------------------------------------------------------
/src/app/root/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './api';
2 |
--------------------------------------------------------------------------------
/src/app/store/create-action.ts:
--------------------------------------------------------------------------------
1 | import {Action} from '@ngrx/store';
2 |
3 | export function createAction(type: string, payload?: any): Action {
4 | return { type, payload };
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/store/reducer-helpers.ts:
--------------------------------------------------------------------------------
1 | export function updateObject(object: T, changes: any): T {
2 | return Object.assign({}, object, changes);
3 | }
4 |
5 | export function updateChildObject(
6 | objects: T[],
7 | shouldModify: (child: T) => boolean,
8 | modifyChild: (child: T) => any
9 | ): T[] {
10 | return objects.map(
11 | child => shouldModify(child) ? updateObject(child, modifyChild(child)) : child
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/store/root-reducer.ts:
--------------------------------------------------------------------------------
1 | import {combineReducers} from '@ngrx/store';
2 | import {routerReducer} from '@ngrx/router-store';
3 | import {compose} from '@ngrx/core/compose';
4 | import {storeLogger} from 'ngrx-store-logger';
5 | import {localStorageSync} from 'ngrx-store-localstorage';
6 |
7 | import {platformsReducer, videoGameListingReducer} from '../video-games/store/reducers';
8 |
9 | const reducers = {
10 | platforms: platformsReducer,
11 | router: routerReducer,
12 | videoGameListing: videoGameListingReducer
13 | };
14 |
15 | const localStorageState = {
16 | keys: ['platforms']
17 | };
18 |
19 | export function rootReducer(state: any, action: any) {
20 | return compose(
21 | storeLogger({
22 | collapsed: true
23 | }),
24 | localStorageSync(localStorageState),
25 | combineReducers
26 | )(reducers)(state, action);
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/test/configure-test-module.function.ts:
--------------------------------------------------------------------------------
1 | import {TestBed, TestModuleMetadata, async} from '@angular/core/testing';
2 |
3 | export function configureTestModule(moduleMetadata: TestModuleMetadata): any {
4 | return async(() => {
5 | TestBed.configureTestingModule(moduleMetadata).compileComponents();
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/test/test-component-support.class.ts:
--------------------------------------------------------------------------------
1 | import {DebugElement, Type} from '@angular/core';
2 | import {ComponentFixture, TestBed} from '@angular/core/testing';
3 |
4 | export class TestComponentSupport {
5 |
6 | public component: T;
7 | private fixture: ComponentFixture;
8 | private debugElement: DebugElement;
9 |
10 | constructor(private componentType: Type) {
11 | this.createComponent();
12 | }
13 |
14 | private createComponent() {
15 | this.fixture = TestBed.createComponent(this.componentType);
16 | this.component = this.fixture.componentInstance;
17 | this.debugElement = this.fixture.debugElement;
18 | }
19 |
20 | public detectChanges(): void {
21 | return this.fixture.detectChanges();
22 | }
23 |
24 | public update(): void {
25 | const component = this.component;
26 |
27 | if (component.ngOnChanges) {
28 | component.ngOnChanges();
29 | }
30 |
31 | this.detectChanges();
32 | }
33 |
34 | public whenStable(): Promise {
35 | return this.fixture.whenStable();
36 | }
37 |
38 | public querySelector(selector: string): HTMLElement {
39 | return this.debugElement.nativeElement.querySelector(selector);
40 | }
41 |
42 | public querySelectorAll(selector: string): any[] {
43 | return this.debugElement.nativeElement.querySelectorAll(selector);
44 | }
45 |
46 | public getAttributeValue(selector: string, attributeName: string): string {
47 | return this.querySelector(selector).getAttribute(attributeName);
48 | }
49 |
50 | public getStyle(selector: string) {
51 | return this.querySelector(selector).style;
52 | }
53 |
54 | public getClassNames(selector: string) {
55 | return this.getAttributeValue(selector, 'class').split(' ');
56 | }
57 |
58 | public getInnerHtml(selector: string) {
59 | return this.querySelector(selector).innerHTML.trim();
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/app/video-games/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-games-container';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/components/video-games-container/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-games-container.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/components/video-games-container/video-games-container.component.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/app/video-games/components/video-games-container/video-games-container.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | height: 100%;
5 |
6 | header {
7 | background-color: #20252F;
8 | color: #79818C;
9 | flex-shrink: 0;
10 | padding: 0 2rem;
11 |
12 | h1 {
13 | cursor: pointer;
14 | outline: 0;
15 | }
16 | }
17 |
18 | section {
19 | color: #A7ACB3;
20 | display: flex;
21 | flex-grow: 1;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/video-games/components/video-games-container/video-games-container.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 |
3 | import {PlatformsStore} from '../../store/platforms/platforms.store';
4 | import {VideoGameListingStore} from '../../store/video-game-listing/video-game-listing.store';
5 |
6 | @Component({
7 | selector: 'app-video-games-container',
8 | templateUrl: './video-games-container.component.html',
9 | styleUrls: ['./video-games-container.component.scss']
10 | })
11 | export class VideoGamesContainerComponent {
12 |
13 | constructor(
14 | private platformsStore: PlatformsStore,
15 | private videoGameListingStore: VideoGameListingStore
16 | ) {
17 | this.platformsStore.retrieve();
18 | this.videoGameListingStore.retrieve();
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-group/button-group.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-group/button-group.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | padding: 0.6rem 1.25rem;
3 | display: block;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-group/button-group.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-button-group',
5 | templateUrl: './button-group.component.html',
6 | styleUrls: ['./button-group.component.scss']
7 | })
8 | export class ButtonGroupComponent { }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-group/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button-group.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-toggle/button-toggle.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-toggle/button-toggle.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | button {
3 | color: #FFF;
4 | cursor: pointer;
5 | background: #F44336;
6 | border-radius: 0.35rem;
7 | border: none;
8 | box-shadow: 0px 5px 0px 0px #D32F2F;
9 | font-size: 1.2em;
10 | margin-left: 0.4rem;
11 | margin-right: 0.4rem;
12 | padding: 0.6rem 1.25rem;
13 |
14 | &:active {
15 | box-shadow: 0px 2px 0px 0px #D32F2F;
16 | transform: translateY(3px);
17 | }
18 |
19 | &.toggled {
20 | color: #F44336;
21 | background: #EEEEEE;
22 | box-shadow: 0px 5px 0px 0px #E0E0E0;
23 |
24 | &:active {
25 | box-shadow: 0px 2px 0px 0px #E0E0E0;
26 | transform: translateY(3px);
27 | }
28 | }
29 |
30 | &:focus {
31 | outline: none;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-toggle/button-toggle.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-button-toggle',
5 | templateUrl: './button-toggle.component.html',
6 | styleUrls: ['./button-toggle.component.scss']
7 | })
8 | export class ButtonToggleComponent {
9 |
10 | @Input()
11 | public isToggled: boolean;
12 |
13 | @Output()
14 | public onToggle = new EventEmitter();
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button-toggle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button-toggle.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button/button.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button/button.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | button {
3 | border-radius: 0.35rem;
4 | border: none;
5 | cursor: pointer;
6 | padding: 0.6rem 1.25rem;
7 | font-size: 1.2em;
8 | margin-left: 0.4rem;
9 | margin-right: 0.4rem;
10 |
11 | &:focus {
12 | outline: none;
13 | }
14 |
15 | &.primary {
16 | color: #FFF;
17 | background-color: #2196F3;
18 | box-shadow: 0px 5px 0px 0px #1976D2;
19 |
20 | &:active {
21 | box-shadow: 0px 2px 0px 0px #1976D2;
22 | transform: translateY(3px);
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button/button.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-button',
5 | templateUrl: './button.component.html',
6 | styleUrls: ['./button.component.scss']
7 | })
8 | export class ButtonComponent {
9 |
10 | @Input()
11 | public type = 'primary';
12 |
13 | @Output()
14 | public onButtonPressed = new EventEmitter();
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './button.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-detail';
2 | export * from './video-game-detail-page';
3 | export * from './youtube-video';
4 | export * from './button-group';
5 | export * from './button';
6 | export * from './button-toggle';
7 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail-page/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-detail-page.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail-page/video-game-detail-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail-page/video-game-detail-page.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | width: 100%;
4 | }
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail-page/video-game-detail-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core';
2 | import {ActivatedRoute} from '@angular/router';
3 | import {Observable} from 'rxjs/Observable';
4 | import 'rxjs/add/operator/switchMap';
5 |
6 | import {IVideoGame} from '../../../interfaces/video-game/video-game.interface';
7 | import {VideoGameListingStore} from '../../../store/video-game-listing/video-game-listing.store';
8 |
9 | @Component({
10 | selector: 'app-video-game-detail-page',
11 | templateUrl: './video-game-detail-page.component.html',
12 | styleUrls: ['./video-game-detail-page.component.scss']
13 | })
14 | export class VideoGameDetailPageComponent implements OnInit {
15 |
16 | public videoGame$: Observable;
17 |
18 | constructor(
19 | private route: ActivatedRoute,
20 | private videoGameListingStore: VideoGameListingStore
21 | ) {
22 |
23 | }
24 |
25 | public ngOnInit() {
26 | this.videoGame$ = this.route.params
27 | .switchMap((params: any) => this.videoGameListingStore.getVideoGame(params.videoGameId));
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-detail.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail/video-game-detail.component.html:
--------------------------------------------------------------------------------
1 | {{videoGame?.title}}
2 | {{videoGame?.description}}
3 |
4 |
5 |
6 | Go Back
7 |
8 |
10 |
11 | {{videoGame.favorite ? 'Unfavorite' : 'Favorite'}}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail/video-game-detail.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | padding: 2rem;
4 | text-align: center;
5 |
6 | h2 {
7 | color: #303A4A;
8 | }
9 |
10 | h2,
11 | p {
12 | margin-top: 0;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/video-game-detail/video-game-detail.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {Location} from '@angular/common';
3 | import {IVideoGame} from '../../../interfaces/video-game/video-game.interface';
4 | import {VideoGameListingStore} from '../../../store/video-game-listing/video-game-listing.store';
5 |
6 | @Component({
7 | selector: 'app-video-game-detail',
8 | templateUrl: './video-game-detail.component.html',
9 | styleUrls: ['./video-game-detail.component.scss']
10 | })
11 | export class VideoGameDetailComponent {
12 |
13 | @Input()
14 | public videoGame: IVideoGame;
15 |
16 | constructor(
17 | private location: Location,
18 | private videoGameListingStore: VideoGameListingStore
19 | ) { }
20 |
21 | public toggleFavorite() {
22 | this.videoGameListingStore.toggleFavorite(this.videoGame.id);
23 | }
24 |
25 | public goBack() {
26 | this.location.back();
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/youtube-video/index.ts:
--------------------------------------------------------------------------------
1 | export * from './youtube-video.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/youtube-video/youtube-video.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/youtube-video/youtube-video.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rangle/angular-ngrx-example/992375cb7f0fe3dac6f864676e1c2799005a49d2/src/app/video-games/detail/components/youtube-video/youtube-video.component.scss
--------------------------------------------------------------------------------
/src/app/video-games/detail/components/youtube-video/youtube-video.component.ts:
--------------------------------------------------------------------------------
1 | import {Input, Component, OnChanges} from '@angular/core';
2 | import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
3 |
4 | @Component({
5 | selector: 'app-youtube-video',
6 | templateUrl: './youtube-video.component.html',
7 | styleUrls: ['./youtube-video.component.scss']
8 | })
9 | export class YouTubeVideoComponent implements OnChanges {
10 |
11 | @Input()
12 | private url: string;
13 |
14 | public trustedUrl: SafeResourceUrl;
15 |
16 | constructor(private sanitizer: DomSanitizer) {
17 |
18 | }
19 |
20 | public ngOnChanges() {
21 | this.trustedUrl = this.getTrustedUrl();
22 | }
23 |
24 | public getTrustedUrl() {
25 | return this.sanitizer.bypassSecurityTrustResourceUrl(this.url);
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/detail.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {RouterModule} from '@angular/router';
4 |
5 | import {LoadingModule} from '../../loading/loading.module';
6 |
7 | import {
8 | ButtonComponent,
9 | ButtonGroupComponent,
10 | ButtonToggleComponent,
11 | VideoGameDetailComponent,
12 | VideoGameDetailPageComponent,
13 | YouTubeVideoComponent,
14 | } from './components';
15 |
16 | @NgModule({
17 | imports: [
18 | CommonModule,
19 | RouterModule,
20 | LoadingModule,
21 | ],
22 | declarations: [
23 | ButtonComponent,
24 | ButtonGroupComponent,
25 | ButtonToggleComponent,
26 | VideoGameDetailComponent,
27 | VideoGameDetailPageComponent,
28 | YouTubeVideoComponent,
29 | ],
30 | providers: []
31 | })
32 | export class VideoGameDetailModule {
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/video-games/detail/detail.routes.ts:
--------------------------------------------------------------------------------
1 | import {Routes} from '@angular/router';
2 |
3 | import {VideoGameDetailPageComponent} from './components';
4 |
5 | export const detailRoutes: Routes = [
6 | {
7 | path: ':videoGameId',
8 | component: VideoGameDetailPageComponent
9 | }
10 | ];
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms';
2 | export * from './video-game';
3 | export * from './video-game-listing';
4 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/platforms/index.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms.interface';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/platforms/platforms.interface.ts:
--------------------------------------------------------------------------------
1 | import {ILoadable} from '../../../loading/interfaces/loadable/loadable';
2 |
3 | export interface IPlatforms extends ILoadable {
4 | readonly list: Array;
5 | }
6 |
7 | export function createDefaultPlatforms(): IPlatforms {
8 | return {
9 | isLoading: false,
10 | loadingError: null,
11 | list: []
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-filters.functions';
2 | export * from './video-game-filters.interface';
3 | export * from './video-game-listing.functions';
4 | export * from './video-game-listing.interface';
5 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/video-game-filters.functions.ts:
--------------------------------------------------------------------------------
1 | import { IVideoGameFilters } from './video-game-filters.interface';
2 |
3 | export function createDefaultVideoGameFilters(): IVideoGameFilters {
4 | return {
5 | platform: null,
6 | favorites: false
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/video-game-filters.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IVideoGameFilters {
2 | platform: string;
3 | favorites: boolean;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/video-game-listing.functions.spec.ts:
--------------------------------------------------------------------------------
1 | import { createVideoGame } from '../video-game';
2 | import { IVideoGameListing } from './video-game-listing.interface';
3 | import {
4 | createDefaultVideoGameListing,
5 | getVideoGame,
6 | getVideoGames
7 | } from './video-game-listing.functions';
8 | import { createDefaultVideoGameFilters } from './video-game-filters.functions';
9 |
10 | describe('when there is a list of 3 unsorted video games', () => {
11 | const unsortedVideoGameListing: IVideoGameListing = {
12 | ...createDefaultVideoGameListing(),
13 | videoGames: [
14 | createVideoGame('1', 'Super Mario'),
15 | createVideoGame('2', 'Legend of Zelda'),
16 | createVideoGame('3', 'Metroid'),
17 | ]
18 | };
19 |
20 | describe('getVideoGames(unsortedVideoGameListing)', () => {
21 | it('returns a list of containing 3 elements', () => {
22 | const videoGames = getVideoGames(unsortedVideoGameListing);
23 | expect(videoGames.length).toEqual(3);
24 | });
25 |
26 | it('returns a list that is sorted by title', () => {
27 | const videoGames = getVideoGames(unsortedVideoGameListing);
28 | expect(videoGames[0].id).toEqual('2');
29 | expect(videoGames[1].id).toEqual('3');
30 | expect(videoGames[2].id).toEqual('1');
31 | });
32 | });
33 |
34 | describe('getVideoGame(unsortedVideoGameListing, "1")', () => {
35 | it('should get the matched video game via its id', () => {
36 | const videoGame = getVideoGame(unsortedVideoGameListing, '1');
37 | expect(videoGame).toEqual(unsortedVideoGameListing.videoGames[0]);
38 | });
39 | });
40 |
41 | describe('getVideoGame(unsortedVideoGameListing, falsy)', () => {
42 | it('returns falsy', () => {
43 | const videoGame = getVideoGame(unsortedVideoGameListing, null);
44 | expect(videoGame).toBeFalsy();
45 | });
46 | });
47 | });
48 |
49 | describe('when there is a list of games from different platforms', () => {
50 | const videoGameListing: IVideoGameListing = {
51 | ...createDefaultVideoGameListing(),
52 | videoGames: [
53 | createVideoGame('1', 'Super Mario Odyssey', 'Nintendo Switch'),
54 | createVideoGame('2', 'Pillars of Eternity', 'PC')
55 | ],
56 | filters: {
57 | ...createDefaultVideoGameFilters(),
58 | platform: 'PC'
59 | }
60 | };
61 |
62 | it('should return a list that matches the specified platform only', () => {
63 | const videoGames = getVideoGames(videoGameListing);
64 | expect(videoGames.length).toEqual(1);
65 | expect(videoGames[0].id).toEqual('2');
66 | });
67 | });
68 |
69 | describe('when there is a list of games from different platforms', () => {
70 | const videoGameListing: IVideoGameListing = {
71 | ...createDefaultVideoGameListing(),
72 | videoGames: [
73 | createVideoGame('1', 'Super Mario Odyssey', 'Nintendo Switch'),
74 | createVideoGame('2', 'Pillars of Eternity', 'PC')
75 | ],
76 | filters: {
77 | ...createDefaultVideoGameFilters(),
78 | platform: 'PC'
79 | }
80 | };
81 |
82 | it('should return a list that matches the specified platform only', () => {
83 | const videoGames = getVideoGames(videoGameListing);
84 | expect(videoGames.length).toEqual(1);
85 | expect(videoGames[0].id).toEqual('2');
86 | });
87 | });
88 |
89 | describe('when there is a list of games with a search query', () => {
90 | const videoGameListing: IVideoGameListing = {
91 | ...createDefaultVideoGameListing(),
92 | videoGames: [
93 | createVideoGame('1', 'Super Mario Odyssey'),
94 | createVideoGame('2', 'Pillars of Eternity')
95 | ],
96 | searchQuery: 'Sup'
97 | };
98 |
99 | it('should return a list that includes the searchQuery string', () => {
100 | const videoGames = getVideoGames(videoGameListing);
101 | expect(videoGames.length).toEqual(1);
102 | expect(videoGames[0].id).toEqual('1');
103 | });
104 | });
105 |
106 | describe('when the list is falsy', () => {
107 | describe('getVideoGame(falsy, "1")', () => {
108 | it('returns falsy', () => {
109 | const videoGame = getVideoGame(null, '1');
110 | expect(videoGame).toBeFalsy();
111 | });
112 | });
113 |
114 | describe('getVideoGames(falsy)', () => {
115 | it('returns an empty list', () => {
116 | const videoGame = getVideoGames(null);
117 | expect(videoGame).toEqual([]);
118 | });
119 | });
120 | });
121 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/video-game-listing.functions.ts:
--------------------------------------------------------------------------------
1 | import { IVideoGameListing } from './video-game-listing.interface';
2 | import { createDefaultVideoGameFilters } from './video-game-filters.functions';
3 | import {
4 | videoGameMatchesFavoritesFilter,
5 | videoGameMatchesPlatformFilter,
6 | videoGameMatchesSearchQuery
7 | } from '../video-game';
8 |
9 | export function createDefaultVideoGameListing(): IVideoGameListing {
10 | return {
11 | isLoading: false,
12 | loadingError: null,
13 | filters: createDefaultVideoGameFilters(),
14 | searchQuery: null,
15 | videoGames: []
16 | };
17 | }
18 |
19 | function getFilteredVideoGames(videoGameListing: IVideoGameListing) {
20 | return videoGameListing.videoGames
21 | .filter(videoGame => videoGameMatchesSearchQuery(videoGame, videoGameListing.searchQuery))
22 | .filter(videoGame => videoGameMatchesPlatformFilter(videoGame, videoGameListing.filters))
23 | .filter(videoGame => videoGameMatchesFavoritesFilter(videoGame, videoGameListing.filters));
24 | }
25 |
26 | export function getVideoGames(videoGameListing: IVideoGameListing) {
27 | return Boolean(videoGameListing) ?
28 | getFilteredVideoGames(videoGameListing).sort(
29 | (videoGameA, videoGameB) => videoGameA.title.localeCompare(videoGameB.title)
30 | ) :
31 | [];
32 | }
33 |
34 | export function getVideoGame(videoGameListing: IVideoGameListing, id: string) {
35 | return Boolean(videoGameListing) ?
36 | videoGameListing.videoGames.find(videoGame => videoGame.id === id) :
37 | null;
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game-listing/video-game-listing.interface.ts:
--------------------------------------------------------------------------------
1 | import { IVideoGame } from '../video-game/video-game.interface';
2 | import { IVideoGameFilters } from './video-game-filters.interface';
3 | import { ILoadable } from '../../../loading/interfaces/loadable/loadable';
4 |
5 | export interface IVideoGameListing extends ILoadable {
6 | filters: IVideoGameFilters;
7 | searchQuery: string;
8 | videoGames: Array;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game.functions';
2 | export * from './video-game.interface';
3 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game/video-game.functions.spec.ts:
--------------------------------------------------------------------------------
1 | import { createDefaultVideoGameFilters, IVideoGameFilters } from '../video-game-listing';
2 | import { IVideoGame } from './video-game.interface';
3 | import {
4 | videoGameMatchesFavoritesFilter,
5 | videoGameMatchesPlatformFilter,
6 | videoGameMatchesSearchQuery
7 | } from './video-game.functions';
8 |
9 | describe('with Super Mario Odyssey', () => {
10 | let superMarioOdyssey: IVideoGame;
11 |
12 | beforeEach(() => {
13 | superMarioOdyssey = {
14 | id: '1',
15 | title: 'Super Mario Odyssey',
16 | description: 'description',
17 | platform: 'Nintendo Switch',
18 | youtubeUrl: 'youtube.com',
19 | favorite: false,
20 | };
21 | });
22 |
23 | describe('videoGameMatchesSearchQuery(superMarioOdyssey, "Super")', () => {
24 | it('returns true', () => {
25 | const isMatched = videoGameMatchesSearchQuery(superMarioOdyssey, 'Super');
26 | expect(isMatched).toEqual(true);
27 | });
28 | });
29 |
30 | describe('videoGameMatchesSearchQuery(superMarioOdyssey, "Zelda")', () => {
31 | it('returns false', () => {
32 | const isMatched = videoGameMatchesSearchQuery(superMarioOdyssey, 'Zelda');
33 | expect(isMatched).toEqual(false);
34 | });
35 | });
36 |
37 | describe('videoGameMatchesSearchQuery(superMarioOdyssey, "")', () => {
38 | it('returns true', () => {
39 | const isMatched = videoGameMatchesSearchQuery(superMarioOdyssey, '');
40 | expect(isMatched).toEqual(true);
41 | });
42 | });
43 |
44 | describe('videoGameMatchesSearchQuery(superMarioOdyssey, falsy)', () => {
45 | it('returns true', () => {
46 | const isMatched = videoGameMatchesSearchQuery(superMarioOdyssey, null);
47 | expect(isMatched).toEqual(true);
48 | });
49 | });
50 |
51 | describe('videoGameMatchesPlatformFilter(superMarioOdyssey, nintendoSwitch)', () => {
52 | it('returns true', () => {
53 | const nintendoSwitch: IVideoGameFilters = {
54 | ...createDefaultVideoGameFilters(),
55 | platform: 'Nintendo Switch',
56 | };
57 |
58 | const isMatched = videoGameMatchesPlatformFilter(superMarioOdyssey, nintendoSwitch);
59 | expect(isMatched).toEqual(true);
60 | });
61 | });
62 |
63 | describe('videoGameMatchesPlatformFilter(superMarioOdyssey, nintendoSwitch)', () => {
64 | it('returns true', () => {
65 | const nintendoSwitch: IVideoGameFilters = {
66 | ...createDefaultVideoGameFilters(),
67 | platform: 'Nintendo Switch',
68 | };
69 |
70 | const isMatched = videoGameMatchesPlatformFilter(superMarioOdyssey, nintendoSwitch);
71 | expect(isMatched).toEqual(true);
72 | });
73 | });
74 |
75 | describe('videoGameMatchesPlatformFilter(superMarioOdyssey, pc)', () => {
76 | it('returns false', () => {
77 | const pc: IVideoGameFilters = {
78 | ...createDefaultVideoGameFilters(),
79 | platform: 'PC',
80 | };
81 |
82 | const isMatched = videoGameMatchesPlatformFilter(superMarioOdyssey, pc);
83 | expect(isMatched).toEqual(false);
84 | });
85 | });
86 |
87 | describe('videoGameMatchesPlatformFilter(superMarioOdyssey, falsy)', () => {
88 | it('returns true', () => {
89 | const isMatched = videoGameMatchesPlatformFilter(superMarioOdyssey, null);
90 | expect(isMatched).toEqual(true);
91 | });
92 | });
93 |
94 | describe('and it is favourited', () => {
95 | beforeEach(() => {
96 | superMarioOdyssey = { ...superMarioOdyssey, favorite: true };
97 | });
98 |
99 | describe('videoGameMatchesFavoritesFilter(superMarioOdyssey, favorites:true)', () => {
100 | it('returns true', () => {
101 | const showFavorites: IVideoGameFilters = {
102 | ...createDefaultVideoGameFilters(),
103 | favorites: true,
104 | };
105 |
106 | const isMatched = videoGameMatchesFavoritesFilter(superMarioOdyssey, showFavorites);
107 | expect(isMatched).toEqual(true);
108 | });
109 | });
110 |
111 | describe('videoGameMatchesFavoritesFilter(superMarioOdyssey, favorites:false)', () => {
112 | it('returns true', () => {
113 | const showFavorites: IVideoGameFilters = {
114 | ...createDefaultVideoGameFilters(),
115 | favorites: false,
116 | };
117 |
118 | const isMatched = videoGameMatchesFavoritesFilter(superMarioOdyssey, showFavorites);
119 | expect(isMatched).toEqual(true);
120 | });
121 | });
122 | });
123 |
124 | describe('and it is not favourited', () => {
125 | beforeEach(() => {
126 | superMarioOdyssey = { ...superMarioOdyssey, favorite: false };
127 | });
128 |
129 | describe('videoGameMatchesFavoritesFilter(superMarioOdyssey, favorites:true)', () => {
130 | it('returns false', () => {
131 | const filters: IVideoGameFilters = {
132 | ...createDefaultVideoGameFilters(),
133 | favorites: true,
134 | };
135 |
136 | const isMatched = videoGameMatchesFavoritesFilter(superMarioOdyssey, filters);
137 | expect(isMatched).toEqual(false);
138 | });
139 | });
140 |
141 | describe('videoGameMatchesFavoritesFilter(superMarioOdyssey, favorites:false)', () => {
142 | it('returns true', () => {
143 | const filters: IVideoGameFilters = {
144 | ...createDefaultVideoGameFilters(),
145 | favorites: false,
146 | };
147 |
148 | const isMatched = videoGameMatchesFavoritesFilter(superMarioOdyssey, filters);
149 | expect(isMatched).toEqual(true);
150 | });
151 | });
152 | });
153 | });
154 |
155 | describe('when the game is falsy', () => {
156 | describe('videoGameMatchesSearchQuery(falsy, "Super")', () => {
157 | it('returns false', () => {
158 | const isMatched = videoGameMatchesSearchQuery(null, 'Super');
159 | expect(isMatched).toEqual(false);
160 | });
161 | });
162 |
163 | describe('videoGameMatchesPlatformFilter(falsy, "Nintendo Switch")', () => {
164 | const nintendoSwitch: IVideoGameFilters = {
165 | ...createDefaultVideoGameFilters(),
166 | platform: 'Nintendo Switch'
167 | };
168 |
169 | it('returns false', () => {
170 | const isMatched = videoGameMatchesPlatformFilter(null, nintendoSwitch);
171 | expect(isMatched).toEqual(false);
172 | });
173 | });
174 | });
175 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game/video-game.functions.ts:
--------------------------------------------------------------------------------
1 | import { IVideoGame } from './video-game.interface';
2 | import { IVideoGameFilters } from '../video-game-listing';
3 |
4 | export function createVideoGame(
5 | id: string,
6 | title: string,
7 | platform?: string,
8 | description?: string,
9 | youtubeUrl?: string,
10 | imageUrl?: string,
11 | favorite = false,
12 | ) {
13 | return {id, title, platform, description, youtubeUrl, imageUrl, favorite};
14 | }
15 |
16 | function textMatchesSearchQuery(text: string, searchQuery: string) {
17 | return text ?
18 | text.toLowerCase().includes(searchQuery.toLowerCase()) :
19 | false;
20 | }
21 |
22 | export function videoGameMatchesSearchQuery(videoGame: IVideoGame, searchQuery: string) {
23 | if (!Boolean(videoGame)) {
24 | return false;
25 | }
26 |
27 | return Boolean(searchQuery) ?
28 | textMatchesSearchQuery(videoGame.title, searchQuery) ||
29 | textMatchesSearchQuery(videoGame.description, searchQuery) :
30 | true;
31 | }
32 |
33 | export function videoGameMatchesPlatformFilter(videoGame: IVideoGame, filters: IVideoGameFilters) {
34 | if (!Boolean(videoGame)) {
35 | return false;
36 | }
37 |
38 | return Boolean(filters) && Boolean(filters.platform) ?
39 | videoGame.platform === filters.platform :
40 | true;
41 | }
42 |
43 | export function videoGameMatchesFavoritesFilter(videoGame: IVideoGame, filters: IVideoGameFilters) {
44 | if (!Boolean(videoGame)) {
45 | return false;
46 | }
47 |
48 | return Boolean(filters) && filters.favorites === true ?
49 | videoGame.favorite :
50 | true;
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/video-games/interfaces/video-game/video-game.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IVideoGame {
2 | readonly id: string;
3 | readonly title: string;
4 | readonly platform?: string;
5 | readonly description?: string;
6 | readonly youtubeUrl?: string;
7 | readonly imageUrl?: string;
8 | readonly favorite?: boolean;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/card/card.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/card/card.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | margin: 0 2rem 2rem 0;
3 | background: #fff;
4 | border-radius: 0.35rem;
5 | box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.19);
6 | cursor: pointer;
7 | height: 23.5rem;
8 | transition: all 0.25s ease-in;
9 |
10 | &:hover,
11 | &:focus {
12 | transform: scale(1.02, 1.02);
13 | box-shadow: 0 1px 9px 2px rgba(0, 0, 0, 0.29);
14 | outline: none;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/card/card.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-card',
5 | templateUrl: './card.component.html',
6 | styleUrls: ['./card.component.scss']
7 | })
8 | export class CardComponent { }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from './card.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/cards/cards.component.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/cards/cards.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-wrap: wrap;
4 | flex-direction: row;
5 | box-sizing: border-box;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/cards/cards.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-cards',
5 | templateUrl: './cards.component.html',
6 | styleUrls: ['./cards.component.scss']
7 | })
8 | export class CardsComponent { }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/cards/index.ts:
--------------------------------------------------------------------------------
1 | export * from './cards.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/favorite-toggle/favorite-toggle.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/favorite-toggle/favorite-toggle.component.scss:
--------------------------------------------------------------------------------
1 | button {
2 | border: none;
3 | background: none;
4 | float: right;
5 | color: #B0BEC5;
6 |
7 | &.active {
8 | color: #F44336;
9 | }
10 |
11 | &:focus {
12 | outline: none;
13 | }
14 |
15 | &:active {
16 | transform: scale(1.2, 1.2);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/favorite-toggle/favorite-toggle.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, Output, EventEmitter } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-favorite-toggle',
5 | templateUrl: './favorite-toggle.component.html',
6 | styleUrls: ['./favorite-toggle.component.scss']
7 | })
8 | export class FavoriteToggleComponent {
9 |
10 | @Input()
11 | public active: boolean;
12 |
13 | @Output()
14 | public onToggle = new EventEmitter();
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/favorite-toggle/index.ts:
--------------------------------------------------------------------------------
1 | export * from './favorite-toggle.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './card';
2 | export * from './cards';
3 | export * from './favorite-toggle';
4 | export * from './video-game-filters';
5 | export * from './video-game-list-item';
6 | export * from './video-game-listing';
7 | export * from './video-game-listing-page';
8 | export * from './video-game-search';
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-filters/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-filters.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-filters/video-game-filters.component.html:
--------------------------------------------------------------------------------
1 |
8 |
9 |
11 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-filters/video-game-filters.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | align-items: center;
4 |
5 | app-favorite-toggle {
6 | margin-left: 0.5rem;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-filters/video-game-filters.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, Output, EventEmitter} from '@angular/core';
2 |
3 | import {IVideoGameFilters} from '../../../interfaces/video-game-listing/video-game-filters.interface';
4 |
5 | @Component({
6 | selector: 'app-video-game-filters',
7 | templateUrl: './video-game-filters.component.html',
8 | styleUrls: ['./video-game-filters.component.scss']
9 | })
10 | export class VideoGameFiltersComponent {
11 |
12 | @Input()
13 | public filters: IVideoGameFilters;
14 |
15 | @Input()
16 | public platforms: Array;
17 |
18 | @Output()
19 | public platformFilterChanged = new EventEmitter();
20 |
21 | @Output()
22 | public favoritesFilterChanged = new EventEmitter();
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-list-item/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-list-item.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-list-item/video-game-list-item.component.html:
--------------------------------------------------------------------------------
1 |
2 | {{videoGame.title}}
3 |
4 | {{videoGame.platform}}
5 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-list-item/video-game-list-item.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: flex;
3 | flex-direction: column;
4 | width: 24rem;
5 | height: 100%;
6 | justify-content: space-between;
7 |
8 | img {
9 | height: 15rem;
10 | width: auto;
11 | border-radius: 0.35rem 0.35rem 0 0;
12 | }
13 |
14 | h3 {
15 | color: #455A64;
16 | margin: 0;
17 | padding: 1rem;
18 | text-align: center;
19 | }
20 |
21 | .details {
22 | padding: 0.75rem 1.25rem;
23 | background-color: #f7f7f9;
24 | border-radius: 0 0 0.35rem 0.35rem;
25 | border-top: 1px solid rgba(0, 0, 0, 0.125);
26 | width: 100%;
27 |
28 | .platform {
29 | color: #78909C;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-list-item/video-game-list-item.component.ts:
--------------------------------------------------------------------------------
1 | import {Input, Component} from '@angular/core';
2 |
3 | import {IVideoGame} from '../../../interfaces/video-game/video-game.interface';
4 | import {VideoGameListingStore} from '../../../store/video-game-listing/video-game-listing.store';
5 |
6 | @Component({
7 | selector: 'app-video-game-list-item',
8 | templateUrl: './video-game-list-item.component.html',
9 | styleUrls: ['./video-game-list-item.component.scss']
10 | })
11 | export class VideoGameListItemComponent {
12 |
13 | @Input()
14 | public videoGame: IVideoGame;
15 |
16 | constructor(private videoGameListingStore: VideoGameListingStore) { }
17 |
18 | public toggleVideoGameFavorite() {
19 | this.videoGameListingStore.toggleFavorite(this.videoGame.id);
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing-page/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-listing-page.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing-page/video-game-listing-page.component.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing-page/video-game-listing-page.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | flex-grow: 1;
4 |
5 | header {
6 | align-items: baseline;
7 | border-bottom: 1px solid #E2E4E5;
8 | display: flex;
9 | flex-direction: row;
10 | flex-shrink: 0;
11 | justify-content: space-between;
12 | padding: 1.25rem 2rem;
13 | }
14 | }
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing-page/video-game-listing-page.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {Observable} from 'rxjs/Observable';
3 |
4 | import {IPlatforms} from '../../../interfaces/platforms/platforms.interface';
5 | import {IVideoGame} from '../../../interfaces/video-game/video-game.interface';
6 | import {PlatformsStore} from '../../../store/platforms/platforms.store';
7 | import {VideoGameListingStore} from '../../../store/video-game-listing/video-game-listing.store';
8 | import {IVideoGameFilters} from '../../../interfaces/video-game-listing/video-game-filters.interface';
9 |
10 | @Component({
11 | selector: 'app-video-game-listing-page',
12 | templateUrl: './video-game-listing-page.component.html',
13 | styleUrls: ['./video-game-listing-page.component.scss']
14 | })
15 | export class VideoGameListingPageComponent {
16 |
17 | constructor(
18 | public platformsStore: PlatformsStore,
19 | public videoGameListingStore: VideoGameListingStore
20 | ) {
21 |
22 | }
23 |
24 | public search(query: string) {
25 | this.videoGameListingStore.search(query);
26 | }
27 |
28 | public filterPlatform(platform: string) {
29 | this.videoGameListingStore.filterPlatform(platform);
30 | }
31 |
32 | public filterFavorites() {
33 | this.videoGameListingStore.toggleFavoriteFilter();
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-listing.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing/video-game-listing.component.html:
--------------------------------------------------------------------------------
1 | 0">
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | There are no games that match your criteria.
11 |
12 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing/video-game-listing.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | background-color: #EEF0F1;
3 | display: flex;
4 | flex-grow: 1;
5 | overflow-y: auto;
6 | overflow-x: hidden;
7 | padding: 1.25rem 2rem;
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-listing/video-game-listing.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 |
3 | import {IVideoGame} from '../../../interfaces/video-game/video-game.interface';
4 |
5 | @Component({
6 | selector: 'app-video-game-listing',
7 | templateUrl: 'video-game-listing.component.html',
8 | styleUrls: ['video-game-listing.component.scss']
9 | })
10 | export class VideoGameListingComponent {
11 |
12 | @Input()
13 | public videoGames: Array;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-search/index.ts:
--------------------------------------------------------------------------------
1 | export * from './video-game-search.component';
2 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-search/video-game-search.component.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-search/video-game-search.component.scss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | flex-grow: 1;
4 | margin-right: 1rem;
5 |
6 | input {
7 | border: 1px solid #E5EAED;
8 | padding: 0.5rem;
9 | width: 100%;
10 | }
11 | }
--------------------------------------------------------------------------------
/src/app/video-games/listing/components/video-game-search/video-game-search.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Output, EventEmitter} from '@angular/core';
2 | import {Subject} from 'rxjs/Subject';
3 | import 'rxjs/add/operator/debounceTime';
4 | import 'rxjs/add/operator/distinctUntilChanged';
5 |
6 | @Component({
7 | selector: 'app-video-game-search',
8 | templateUrl: 'video-game-search.component.html',
9 | styleUrls: ['video-game-search.component.scss']
10 | })
11 | export class VideoGameSearchComponent {
12 |
13 | @Output()
14 | private queryChanged = new EventEmitter();
15 |
16 | private query$: Subject = new Subject();
17 |
18 | constructor() {
19 | this.query$
20 | .debounceTime(300)
21 | .distinctUntilChanged()
22 | .subscribe(query => this.queryChanged.emit(query));
23 | }
24 |
25 | public onChange(query: string) {
26 | this.query$.next(query);
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/listing.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {RouterModule} from '@angular/router';
4 |
5 | import {LoadingModule} from '../../loading/loading.module';
6 | import {
7 | CardComponent,
8 | CardsComponent,
9 | FavoriteToggleComponent,
10 | VideoGameFiltersComponent,
11 | VideoGameListingComponent,
12 | VideoGameListingPageComponent,
13 | VideoGameListItemComponent,
14 | VideoGameSearchComponent,
15 | } from './components';
16 |
17 | @NgModule({
18 | imports: [
19 | CommonModule,
20 | RouterModule,
21 | LoadingModule
22 | ],
23 | declarations: [
24 | CardComponent,
25 | CardsComponent,
26 | FavoriteToggleComponent,
27 | VideoGameFiltersComponent,
28 | VideoGameListingComponent,
29 | VideoGameListingPageComponent,
30 | VideoGameListItemComponent,
31 | VideoGameSearchComponent,
32 | ],
33 | providers: []
34 | })
35 | export class VideoGameListingModule {
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/app/video-games/listing/listing.routes.ts:
--------------------------------------------------------------------------------
1 | import {Routes} from '@angular/router';
2 |
3 | import {VideoGameListingPageComponent} from './components';
4 |
5 | export const listingRoutes: Routes = [
6 | {
7 | path: '',
8 | redirectTo: 'listing',
9 | pathMatch: 'full',
10 | },
11 | {
12 | path: 'listing',
13 | component: VideoGameListingPageComponent
14 | }
15 | ];
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/app/video-games/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms.service';
2 | export * from './video-games.service';
3 |
--------------------------------------------------------------------------------
/src/app/video-games/services/platforms.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Observable} from 'rxjs/Observable';
3 |
4 | import {ApiService} from '../../root/services/api/api.service';
5 |
6 | @Injectable()
7 | export class PlatformsService {
8 |
9 | constructor(private apiService: ApiService) {
10 |
11 | }
12 |
13 | public getAll(): Observable> {
14 | return this.apiService.get('/5895c55a290000a31a3f4356');
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/video-games/services/video-games.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Observable} from 'rxjs/Observable';
3 |
4 | import {ApiService} from '../../root/services/api/api.service';
5 | import {IVideoGame} from '../interfaces/video-game/video-game.interface';
6 |
7 | @Injectable()
8 | export class VideoGamesService {
9 |
10 | constructor(private apiService: ApiService) {
11 |
12 | }
13 |
14 | public getAll(): Observable> {
15 | return this.apiService.get('/5895c537290000a31a3f4355');
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/video-games/store/effects.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms/platforms.effects';
2 | export * from './video-game-listing/video-game-listing.effects';
3 |
--------------------------------------------------------------------------------
/src/app/video-games/store/platforms/platforms.effects.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Effect, Actions} from '@ngrx/effects';
3 | import {Observable} from 'rxjs/Observable';
4 |
5 | import {PlatformsStore} from './platforms.store';
6 | import {createAction} from '../../../store/create-action';
7 | import {PlatformsService} from '../../services/platforms.service';
8 |
9 | @Injectable()
10 | export class PlatformsEffects {
11 |
12 | @Effect()
13 | private retrieve$ = this.actions$
14 | .ofType(PlatformsStore.RETRIEVE)
15 | .mergeMap(() => this.platformsService.getAll()
16 | .map(platforms => createAction(PlatformsStore.RETRIEVE_SUCCESS, { platforms }))
17 | .catch(error => Observable.of(createAction(PlatformsStore.RETRIEVE_ERROR, { error })))
18 | );
19 |
20 | constructor(
21 | private actions$: Actions,
22 | private platformsService: PlatformsService
23 | ) {
24 |
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/video-games/store/platforms/platforms.reducer.spec.ts:
--------------------------------------------------------------------------------
1 | import {createAction} from '../../../store/create-action';
2 | import {platformsReducer} from './platforms.reducer';
3 | import {PlatformsStore} from './platforms.store';
4 | import {createDefaultPlatforms} from '../../interfaces/platforms/platforms.interface';
5 |
6 | describe('platformsReducer(falsy, unknownAction)', () => {
7 | const unknownAction = createAction('UNKNOWN');
8 |
9 | it('returns the default state', () => {
10 | const platforms = platformsReducer(null, unknownAction);
11 | expect(platforms).toEqual(createDefaultPlatforms());
12 | });
13 | });
14 |
15 | describe('platformsReducer(platforms, retrieveAction)', () => {
16 | const platforms = {...createDefaultPlatforms(), loadingError: 'Error' };
17 | const retrieveAction = createAction(PlatformsStore.RETRIEVE);
18 |
19 | it('sets isLoading to true', () => {
20 | const newPlatforms = platformsReducer(platforms, retrieveAction);
21 | expect(newPlatforms.isLoading).toEqual(true);
22 | });
23 |
24 | it('clears out existing errors', () => {
25 | const newPlatforms = platformsReducer(platforms, retrieveAction);
26 | expect(newPlatforms.loadingError).toBeNull();
27 | });
28 | });
29 |
30 | describe('platformsReducer(platforms, retrieveSuccessAction)', () => {
31 | const platforms = {...createDefaultPlatforms(), isLoading: true};
32 | const retrieveSuccessAction = createAction(PlatformsStore.RETRIEVE_SUCCESS, {
33 | platforms: ['Nintendo Switch', 'PC']
34 | });
35 |
36 | it('set the platforms list', () => {
37 | const newPlatforms = platformsReducer(platforms, retrieveSuccessAction);
38 | expect(newPlatforms.list).toEqual(['Nintendo Switch', 'PC']);
39 | });
40 |
41 | it('should set isLoading to false', () => {
42 | const newPlatforms = platformsReducer(platforms, retrieveSuccessAction);
43 | expect(newPlatforms.isLoading).toEqual(false);
44 | });
45 | });
46 |
47 | describe('platformsReducer(platforms, retrieveErrorAction)', () => {
48 | const platforms = {...createDefaultPlatforms(), isLoading: true};
49 | const retrieveErrorAction = createAction(PlatformsStore.RETRIEVE_ERROR, {
50 | error: 'Error Message'
51 | });
52 |
53 | it('set the loading error', () => {
54 | const newPlatforms = platformsReducer(platforms, retrieveErrorAction);
55 | expect(newPlatforms.loadingError).toEqual('Error Message');
56 | });
57 |
58 | it('should set isLoading to false', () => {
59 | const newPlatforms = platformsReducer(platforms, retrieveErrorAction);
60 | expect(newPlatforms.isLoading).toEqual(false);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/src/app/video-games/store/platforms/platforms.reducer.ts:
--------------------------------------------------------------------------------
1 | import {Action} from '@ngrx/store';
2 |
3 | import {
4 | createDefaultPlatforms,
5 | IPlatforms
6 | } from '../../interfaces/platforms/platforms.interface';
7 | import {PlatformsStore} from './platforms.store';
8 |
9 | export function platformsReducer(state: IPlatforms, action: Action): IPlatforms {
10 | state = state || createDefaultPlatforms();
11 |
12 | switch (action.type) {
13 | case PlatformsStore.RETRIEVE:
14 | return {
15 | ...state,
16 | isLoading: true,
17 | loadingError: null
18 | };
19 | case PlatformsStore.RETRIEVE_SUCCESS:
20 | return {
21 | ...state,
22 | isLoading: false,
23 | list: action.payload.platforms
24 | };
25 | case PlatformsStore.RETRIEVE_ERROR:
26 | return {
27 | ...state,
28 | isLoading: false,
29 | loadingError: action.payload.error
30 | };
31 | default:
32 | return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/video-games/store/platforms/platforms.store.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Store} from '@ngrx/store';
3 | import {Observable} from 'rxjs/Observable';
4 |
5 | import {IPlatforms} from '../../interfaces/platforms/platforms.interface';
6 | import {IAppState} from '../../../interfaces/app-state.interface';
7 | import {createAction} from '../../../store/create-action';
8 |
9 | @Injectable()
10 | export class PlatformsStore {
11 |
12 | public static RETRIEVE = 'PLATFORMS_RETRIEVE';
13 | public static RETRIEVE_SUCCESS = 'PLATFORMS_RETRIEVE_SUCCESS';
14 | public static RETRIEVE_ERROR = 'PLATFORMS_RETRIEVE_ERROR';
15 |
16 | constructor(private store: Store) {
17 |
18 | }
19 |
20 | public getPlatforms(): Observable {
21 | return this.store.select(appState => appState.platforms);
22 | }
23 |
24 | public retrieve() {
25 | this.store.dispatch(createAction(PlatformsStore.RETRIEVE));
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/video-games/store/reducers.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms/platforms.reducer';
2 | export * from './video-game-listing/video-game-listing.reducer';
3 |
--------------------------------------------------------------------------------
/src/app/video-games/store/stores.ts:
--------------------------------------------------------------------------------
1 | export * from './platforms/platforms.store';
2 | export * from './video-game-listing/video-game-listing.store';
3 |
--------------------------------------------------------------------------------
/src/app/video-games/store/video-game-listing/video-game-listing.effects.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Effect, Actions} from '@ngrx/effects';
3 | import {Observable} from 'rxjs/Observable';
4 | import 'rxjs/add/observable/of';
5 | import 'rxjs/add/operator/catch';
6 | import 'rxjs/add/operator/mergeMap';
7 |
8 | import {VideoGameListingStore} from './video-game-listing.store';
9 | import {createAction} from '../../../store/create-action';
10 | import {VideoGamesService} from '../../services/video-games.service';
11 |
12 | @Injectable()
13 | export class VideoGameListingEffects {
14 |
15 | @Effect()
16 | private retrieve$ = this.actions$
17 | .ofType(VideoGameListingStore.RETRIEVE)
18 | .mergeMap(() => this.videoGamesService.getAll()
19 | .map(videoGames => createAction(VideoGameListingStore.RETRIEVE_SUCCESS, { videoGames }))
20 | .catch(error => Observable.of(createAction(VideoGameListingStore.RETRIEVE_ERROR, { error })))
21 | );
22 |
23 | constructor(
24 | private actions$: Actions,
25 | private videoGamesService: VideoGamesService
26 | ) {
27 |
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/video-games/store/video-game-listing/video-game-listing.reducer.spec.ts:
--------------------------------------------------------------------------------
1 | import { createAction } from '../../../store/create-action';
2 | import { videoGameListingReducer } from './video-game-listing.reducer';
3 | import { VideoGameListingStore } from './video-game-listing.store';
4 | import {
5 | createDefaultVideoGameListing,
6 | createVideoGame,
7 | IVideoGameListing,
8 | } from '../../interfaces';
9 |
10 | describe('videoGameListingReducer(falsy, unknownAction)', () => {
11 | const unknownAction = createAction('UNKNOWN');
12 |
13 | it('returns the default state', () => {
14 | const videoGameListing = videoGameListingReducer(null, unknownAction);
15 | expect(videoGameListing).toEqual(createDefaultVideoGameListing());
16 | });
17 | });
18 |
19 | describe('videoGameListingReducer(videoGameListing, retrieveAction)', () => {
20 | const videoGameListing: IVideoGameListing = {
21 | ...createDefaultVideoGameListing(),
22 | loadingError: 'Error'
23 | };
24 | const retrieveAction = createAction(VideoGameListingStore.RETRIEVE);
25 |
26 | it('sets isLoading to true', () => {
27 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveAction);
28 | expect(newVideoGameListing.isLoading).toEqual(true);
29 | });
30 |
31 | it('clears out existing errors', () => {
32 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveAction);
33 | expect(newVideoGameListing.loadingError).toBeNull();
34 | });
35 | });
36 |
37 | describe('videoGameListingReducer(videoGameListing, retrieveSuccessAction)', () => {
38 | const videoGameListing = {
39 | ...createDefaultVideoGameListing(),
40 | isLoading: true
41 | };
42 | const retrieveSuccessAction = createAction(VideoGameListingStore.RETRIEVE_SUCCESS, {
43 | videoGames: [createVideoGame('1', 'Super Mario')]
44 | });
45 |
46 | it('set the videoGameListing list', () => {
47 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveSuccessAction);
48 | expect(newVideoGameListing.videoGames).toEqual([createVideoGame('1', 'Super Mario')]);
49 | });
50 |
51 | it('should set isLoading to false', () => {
52 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveSuccessAction);
53 | expect(newVideoGameListing.isLoading).toEqual(false);
54 | });
55 | });
56 |
57 | describe('videoGameListingReducer(videoGameListing, retrieveErrorAction)', () => {
58 | const videoGameListing = {
59 | ...createDefaultVideoGameListing(),
60 | isLoading: true
61 | };
62 | const retrieveErrorAction = createAction(VideoGameListingStore.RETRIEVE_ERROR, {
63 | error: 'Error Message'
64 | });
65 |
66 | it('set the loading error', () => {
67 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveErrorAction);
68 | expect(newVideoGameListing.loadingError).toEqual('Error Message');
69 | });
70 |
71 | it('should set isLoading to false', () => {
72 | const newVideoGameListing = videoGameListingReducer(videoGameListing, retrieveErrorAction);
73 | expect(newVideoGameListing.isLoading).toEqual(false);
74 | });
75 | });
76 |
77 | describe('videoGameListingReducer(videoGameListing, searchAction)', () => {
78 | const videoGameListing = createDefaultVideoGameListing();
79 | const searchAction = createAction(VideoGameListingStore.SEARCH, {
80 | query: 'Super'
81 | });
82 |
83 | it('set the search query', () => {
84 | const newVideoGameListing = videoGameListingReducer(videoGameListing, searchAction);
85 | expect(newVideoGameListing.searchQuery).toEqual('Super');
86 | });
87 | });
88 |
89 | describe('videoGameListingReducer(videoGameListing, filterPlatformAction)', () => {
90 | const videoGameListing = createDefaultVideoGameListing();
91 | const filterPlatformAction = createAction(VideoGameListingStore.FILTER_PLATFORM, {
92 | platform: 'Nintendo Switch'
93 | });
94 |
95 | it('set the search query', () => {
96 | const newVideoGameListing = videoGameListingReducer(videoGameListing, filterPlatformAction);
97 | expect(newVideoGameListing.filters.platform).toEqual('Nintendo Switch');
98 | });
99 | });
100 |
101 | describe('videoGameListingReducer(videoGameListing, filterFavoritesAction)', () => {
102 | const videoGameListing = createDefaultVideoGameListing();
103 | const filterFavoritesAction = createAction(VideoGameListingStore.TOGGLE_FAVORITE_FILTER);
104 |
105 | it('favorites filter should be false', () => {
106 | expect(videoGameListing.filters.favorites).toEqual(false);
107 | });
108 |
109 | it('favorites filter should be true', () => {
110 | const newVideoGameListing = videoGameListingReducer(videoGameListing, filterFavoritesAction);
111 | expect(newVideoGameListing.filters.favorites).toEqual(true);
112 | });
113 | });
114 |
115 | describe('videoGameListingReducer(videoGameListing, toggleFavouriteAction)', () => {
116 | const videoGameListing = {
117 | ...createDefaultVideoGameListing(),
118 | videoGames: [
119 | createVideoGame('1', 'Legend of Zelda')
120 | ]
121 | };
122 |
123 | it('should be false', () => {
124 | expect(videoGameListing.videoGames[0].favorite).toEqual(false);
125 | });
126 |
127 | it('should set the "Legend of Zelda" favorite property to true', () => {
128 | const toggleFavoriteAction = createAction(VideoGameListingStore.TOGGLE_FAVORITE, {id: '1'});
129 | const newVideoGameListing = videoGameListingReducer(videoGameListing, toggleFavoriteAction);
130 | expect(newVideoGameListing.videoGames[0].favorite).toEqual(true);
131 | });
132 | });
133 |
--------------------------------------------------------------------------------
/src/app/video-games/store/video-game-listing/video-game-listing.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '@ngrx/store';
2 |
3 | import { updateChildObject } from '../../../store/reducer-helpers';
4 | import { VideoGameListingStore } from './video-game-listing.store';
5 | import {
6 | createDefaultVideoGameListing,
7 | IVideoGameListing,
8 | } from '../../interfaces';
9 |
10 | export function videoGameListingReducer(
11 | state: IVideoGameListing,
12 | action: Action
13 | ): IVideoGameListing {
14 | state = state || createDefaultVideoGameListing();
15 |
16 | switch (action.type) {
17 | case VideoGameListingStore.RETRIEVE:
18 | return {
19 | ...state,
20 | isLoading: true,
21 | loadingError: null
22 | };
23 | case VideoGameListingStore.RETRIEVE_SUCCESS:
24 | const videoGames = action.payload.videoGames
25 | .map(game => ({...game, favorite: false}));
26 |
27 | return {
28 | ...state,
29 | isLoading: false,
30 | videoGames: videoGames
31 | };
32 | case VideoGameListingStore.RETRIEVE_ERROR:
33 | return {
34 | ...state,
35 | isLoading: false,
36 | loadingError: action.payload.error
37 | };
38 | case VideoGameListingStore.SEARCH:
39 | return {
40 | ...state,
41 | searchQuery: action.payload.query
42 | };
43 | case VideoGameListingStore.FILTER_PLATFORM:
44 | return {
45 | ...state,
46 | filters: {
47 | ...state.filters,
48 | platform: action.payload.platform
49 | }
50 | };
51 | case VideoGameListingStore.TOGGLE_FAVORITE_FILTER:
52 | return {
53 | ...state,
54 | filters: {
55 | ...state.filters,
56 | favorites: !state.filters.favorites
57 | }
58 | };
59 | case VideoGameListingStore.TOGGLE_FAVORITE:
60 | return {
61 | ...state,
62 | videoGames: updateChildObject(
63 | state.videoGames,
64 | (game) => game.id === action.payload.id,
65 | (game) => ({favorite: !game.favorite}),
66 | )
67 | };
68 | default:
69 | return state;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/video-games/store/video-game-listing/video-game-listing.store.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Store } from '@ngrx/store';
3 | import { Observable } from 'rxjs/Observable';
4 | import 'rxjs/add/operator/map';
5 |
6 | import { IAppState } from '../../../interfaces/app-state.interface';
7 | import { createAction } from '../../../store/create-action';
8 | import {
9 | getVideoGames,
10 | getVideoGame,
11 | IVideoGame,
12 | IVideoGameFilters,
13 | IVideoGameListing,
14 | } from '../../interfaces';
15 |
16 | @Injectable()
17 | export class VideoGameListingStore {
18 |
19 | public static RETRIEVE = 'VIDEO_GAME_LISTING_RETRIEVE';
20 | public static RETRIEVE_SUCCESS = 'VIDEO_GAME_LISTING_RETRIEVE_SUCCESS';
21 | public static RETRIEVE_ERROR = 'VIDEO_GAME_LISTING_RETRIEVE_ERROR';
22 | public static SEARCH = 'VIDEO_GAME_LISTING_SEARCH';
23 | public static FILTER_PLATFORM = 'VIDEO_GAME_LISTING_FILTER_PLATFORM';
24 | public static TOGGLE_FAVORITE_FILTER = 'VIDEO_GAME_LISTING_FILTER_FAVORITES';
25 | public static TOGGLE_FAVORITE = 'VIDEO_GAME_TOGGLE_FAVORITE';
26 |
27 | constructor(private store: Store) {}
28 |
29 | public getVideoGameListing(): Observable {
30 | return this.store.select(appState => appState.videoGameListing);
31 | }
32 |
33 | public getVideoGameFilters(): Observable {
34 | return this.getVideoGameListing()
35 | .map(videoGameListing => videoGameListing.filters);
36 | }
37 |
38 | public getVideoGames(): Observable> {
39 | return this.getVideoGameListing()
40 | .map(videoGameListing => getVideoGames(videoGameListing));
41 | }
42 |
43 | public getVideoGame(id: string): Observable {
44 | return this.getVideoGameListing()
45 | .map(videoGameListing => getVideoGame(videoGameListing, id));
46 | }
47 |
48 | public retrieve() {
49 | this.store.dispatch(createAction(VideoGameListingStore.RETRIEVE));
50 | }
51 |
52 | public search(query: string) {
53 | this.store.dispatch(createAction(VideoGameListingStore.SEARCH, {query}));
54 | }
55 |
56 | public filterPlatform(platform: string) {
57 | this.store.dispatch(createAction(VideoGameListingStore.FILTER_PLATFORM, {platform}));
58 | }
59 |
60 | public toggleFavoriteFilter() {
61 | this.store.dispatch(createAction(VideoGameListingStore.TOGGLE_FAVORITE_FILTER));
62 | }
63 |
64 | public toggleFavorite(id: string) {
65 | this.store.dispatch(createAction(VideoGameListingStore.TOGGLE_FAVORITE, {id}));
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/app/video-games/video-games.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {RouterModule} from '@angular/router';
4 | import {EffectsModule} from '@ngrx/effects';
5 |
6 | import {
7 | VideoGamesContainerComponent
8 | } from './components';
9 |
10 | import {PlatformsService, VideoGamesService} from './services';
11 | import {VideoGameListingModule} from './listing/listing.module';
12 | import {VideoGameDetailModule} from './detail/detail.module';
13 | import {PlatformsEffects, VideoGameListingEffects} from '../video-games/store/effects';
14 |
15 | @NgModule({
16 | imports: [
17 | CommonModule,
18 | RouterModule,
19 | VideoGameListingModule,
20 | VideoGameDetailModule,
21 | EffectsModule.run(PlatformsEffects),
22 | EffectsModule.run(VideoGameListingEffects),
23 | ],
24 | declarations: [
25 | VideoGamesContainerComponent
26 | ],
27 | providers: [
28 | PlatformsService,
29 | VideoGamesService
30 | ]
31 | })
32 | export class VideoGamesModule {
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/video-games/video-games.routes.ts:
--------------------------------------------------------------------------------
1 | import {Routes} from '@angular/router';
2 |
3 | import {listingRoutes} from './listing/listing.routes';
4 | import {detailRoutes} from './detail/detail.routes';
5 | import {VideoGamesContainerComponent} from './components';
6 |
7 | export const videoGamesRoutes: Routes = [
8 | {
9 | path: '',
10 | redirectTo: 'videoGames',
11 | pathMatch: 'full',
12 | },
13 | {
14 | path: 'videoGames',
15 | component: VideoGamesContainerComponent,
16 | children: [
17 | ...listingRoutes,
18 | ...detailRoutes
19 | ]
20 | }
21 | ];
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rangle/angular-ngrx-example/992375cb7f0fe3dac6f864676e1c2799005a49d2/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rangle/angular-ngrx-example/992375cb7f0fe3dac6f864676e1c2799005a49d2/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Angular 2 Ngrx Example - Video Games
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Loading...
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
2 | import {enableProdMode} from '@angular/core';
3 |
4 | import {environment} from './environments/environment';
5 | import {RootModule} from './app/root/root.module';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic().bootstrapModule(RootModule);
12 |
--------------------------------------------------------------------------------
/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/docs/ts/latest/guide/browser-support.html
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/array';
32 | // import 'core-js/es6/regexp';
33 | // import 'core-js/es6/map';
34 | // import 'core-js/es6/set';
35 |
36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
37 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
38 |
39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */
40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
41 |
42 |
43 | /** Evergreen browsers require these. **/
44 | import 'core-js/es6/reflect';
45 | import 'core-js/es7/reflect';
46 |
47 |
48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/
49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
50 |
51 |
52 |
53 | /***************************************************************************************************
54 | * Zone JS is required by Angular itself.
55 | */
56 | import 'zone.js/dist/zone'; // Included with Angular CLI.
57 |
58 |
59 |
60 | /***************************************************************************************************
61 | * APPLICATION IMPORTS
62 | */
63 |
64 | /**
65 | * Date, currency, decimal and percent pipes.
66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
67 | */
68 | // import 'intl'; // Run `npm install --save intl`.
69 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | height: 100%;
3 | }
4 |
5 | body {
6 | font-family: 'Open Sans', sans-serif;
7 | margin: 0;
8 | padding: 0;
9 | -webkit-overflow-scrolling: touch;
10 | }
11 |
12 | * {
13 | box-sizing: border-box;
14 | }
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/dist/long-stack-trace-zone';
2 | import 'zone.js/dist/proxy.js';
3 | import 'zone.js/dist/sync-test';
4 | import 'zone.js/dist/jasmine-patch';
5 | import 'zone.js/dist/async-test';
6 | import 'zone.js/dist/fake-async-test';
7 | import { getTestBed } from '@angular/core/testing';
8 | import {
9 | BrowserDynamicTestingModule,
10 | platformBrowserDynamicTesting
11 | } from '@angular/platform-browser-dynamic/testing';
12 |
13 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
14 | declare var __karma__: any;
15 | declare var require: any;
16 |
17 | // Prevent Karma from running prematurely.
18 | __karma__.loaded = function () {};
19 |
20 | // First, initialize the Angular testing environment.
21 | getTestBed().initTestEnvironment(
22 | BrowserDynamicTestingModule,
23 | platformBrowserDynamicTesting()
24 | );
25 | // Then we find all the tests.
26 | const context = require.context('./', true, /\.spec\.ts$/);
27 | // And load the modules.
28 | context.keys().map(context);
29 | // Finally, start Karma to run the tests.
30 | __karma__.start();
31 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016",
10 | "dom"
11 | ],
12 | "outDir": "../out-tsc/app",
13 | "target": "es5",
14 | "module": "es2015",
15 | "baseUrl": "",
16 | "types": []
17 | },
18 | "exclude": [
19 | "test.ts",
20 | "**/*.spec.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016",
10 | "dom"
11 | ],
12 | "outDir": "../out-tsc/spec",
13 | "module": "commonjs",
14 | "target": "es5",
15 | "baseUrl": "",
16 | "types": [
17 | "jasmine",
18 | "node"
19 | ]
20 | },
21 | "files": [
22 | "test.ts"
23 | ],
24 | "include": [
25 | "**/*.spec.ts"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "es5",
11 | "lib": [
12 | "es2016",
13 | "dom"
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "callable-types": true,
7 | "class-name": true,
8 | "comment-format": [
9 | true,
10 | "check-space"
11 | ],
12 | "curly": true,
13 | "eofline": true,
14 | "forin": true,
15 | "import-blacklist": [true, "rxjs"],
16 | "import-spacing": true,
17 | "indent": [
18 | true,
19 | "spaces"
20 | ],
21 | "interface-over-type-literal": true,
22 | "label-position": true,
23 | "max-line-length": [
24 | true,
25 | 140
26 | ],
27 | "member-access": true,
28 | "member-ordering": [
29 | true,
30 | "static-before-instance",
31 | "variables-before-functions"
32 | ],
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-construct": true,
44 | "no-debugger": true,
45 | "no-duplicate-variable": true,
46 | "no-empty": false,
47 | "no-empty-interface": true,
48 | "no-eval": true,
49 | "no-inferrable-types": true,
50 | "no-shadowed-variable": true,
51 | "no-string-literal": false,
52 | "no-string-throw": true,
53 | "no-switch-case-fall-through": true,
54 | "no-trailing-whitespace": true,
55 | "no-unused-expression": true,
56 | "no-use-before-declare": true,
57 | "no-var-keyword": true,
58 | "object-literal-sort-keys": false,
59 | "one-line": [
60 | true,
61 | "check-open-brace",
62 | "check-catch",
63 | "check-else",
64 | "check-whitespace"
65 | ],
66 | "prefer-const": true,
67 | "quotemark": [
68 | true,
69 | "single"
70 | ],
71 | "radix": true,
72 | "semicolon": [
73 | "always"
74 | ],
75 | "triple-equals": [
76 | true,
77 | "allow-null-check"
78 | ],
79 | "typedef-whitespace": [
80 | true,
81 | {
82 | "call-signature": "nospace",
83 | "index-signature": "nospace",
84 | "parameter": "nospace",
85 | "property-declaration": "nospace",
86 | "variable-declaration": "nospace"
87 | }
88 | ],
89 | "typeof-compare": true,
90 | "unified-signatures": true,
91 | "variable-name": false,
92 | "whitespace": [
93 | true,
94 | "check-branch",
95 | "check-decl",
96 | "check-operator",
97 | "check-separator",
98 | "check-type"
99 | ],
100 |
101 | "directive-selector": [true, "attribute", "app", "camelCase"],
102 | "component-selector": [true, "element", "app", "kebab-case"],
103 | "use-input-property-decorator": true,
104 | "use-output-property-decorator": true,
105 | "use-host-property-decorator": true,
106 | "no-input-rename": true,
107 | "no-output-rename": true,
108 | "use-life-cycle-interface": true,
109 | "use-pipe-transform-interface": true,
110 | "component-class-suffix": true,
111 | "directive-class-suffix": true,
112 | "no-access-missing-member": false,
113 | "templates-use-public": true,
114 | "invoke-injectable": true
115 | }
116 | }
117 |
--------------------------------------------------------------------------------