├── .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 |
7 | 8 |
-------------------------------------------------------------------------------- /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 |
3 | 4 | 8 | 9 |
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 | 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 | --------------------------------------------------------------------------------