├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── angular_ecommerce_img_1.png ├── angular_ecommerce_img_2.png ├── angular_ecommerce_img_3.png ├── angular_ecommerce_img_4.png ├── angular_ecommerce_img_5.png ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routes.ts │ ├── core │ │ ├── core.module.ts │ │ ├── data │ │ │ ├── brands.ts │ │ │ └── phones.ts │ │ ├── enums │ │ │ ├── event-types.ts │ │ │ └── order.ts │ │ ├── footer │ │ │ ├── footer.component.html │ │ │ ├── footer.component.scss │ │ │ ├── footer.component.spec.ts │ │ │ └── footer.component.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ ├── header.component.spec.ts │ │ │ └── header.component.ts │ │ ├── layout-mode │ │ │ ├── layout-mode.component.html │ │ │ ├── layout-mode.component.scss │ │ │ ├── layout-mode.component.spec.ts │ │ │ └── layout-mode.component.ts │ │ ├── models │ │ │ └── Product.ts │ │ ├── not-found │ │ │ ├── not-found.component.html │ │ │ ├── not-found.component.scss │ │ │ ├── not-found.component.spec.ts │ │ │ └── not-found.component.ts │ │ ├── pipes │ │ │ ├── brand.pipe.spec.ts │ │ │ ├── brand.pipe.ts │ │ │ ├── order-by-price.pipe.spec.ts │ │ │ ├── order-by-price.pipe.ts │ │ │ ├── price-formatter.pipe.spec.ts │ │ │ ├── price-formatter.pipe.ts │ │ │ ├── shorten-title.pipe.spec.ts │ │ │ └── shorten-title.pipe.ts │ │ └── pure-pipes │ │ │ ├── brand.pure.pipe.ts │ │ │ └── order-by-price.pure.pipe.ts │ ├── filters │ │ ├── brand-filter │ │ │ ├── brand-filter.component.html │ │ │ ├── brand-filter.component.scss │ │ │ ├── brand-filter.component.spec.ts │ │ │ └── brand-filter.component.ts │ │ ├── filters.module.ts │ │ └── price-filter │ │ │ ├── price-filter.component.html │ │ │ ├── price-filter.component.scss │ │ │ ├── price-filter.component.spec.ts │ │ │ └── price-filter.component.ts │ ├── pages │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ └── home.component.ts │ │ ├── pages.module.ts │ │ ├── pages.route.ts │ │ ├── product-detail-page │ │ │ ├── product-detail-page.component.html │ │ │ ├── product-detail-page.component.scss │ │ │ ├── product-detail-page.component.spec.ts │ │ │ └── product-detail-page.component.ts │ │ └── shopping-cart-page │ │ │ ├── shopping-cart-page.component.html │ │ │ ├── shopping-cart-page.component.scss │ │ │ ├── shopping-cart-page.component.spec.ts │ │ │ └── shopping-cart-page.component.ts │ ├── pagination │ │ ├── pagination.module.ts │ │ ├── pagination.pipe.spec.ts │ │ ├── pagination.pipe.ts │ │ └── pagination │ │ │ ├── pagination.component.html │ │ │ ├── pagination.component.scss │ │ │ ├── pagination.component.spec.ts │ │ │ └── pagination.component.ts │ ├── products │ │ ├── product-detail │ │ │ ├── product-detail.component.html │ │ │ ├── product-detail.component.scss │ │ │ ├── product-detail.component.spec.ts │ │ │ └── product-detail.component.ts │ │ ├── product-list │ │ │ ├── product-list.component.html │ │ │ ├── product-list.component.scss │ │ │ ├── product-list.component.spec.ts │ │ │ └── product-list.component.ts │ │ ├── product-slider-dots │ │ │ ├── product-slider-dots.component.html │ │ │ ├── product-slider-dots.component.scss │ │ │ ├── product-slider-dots.component.spec.ts │ │ │ └── product-slider-dots.component.ts │ │ ├── product-slider │ │ │ ├── product-slider.component.html │ │ │ ├── product-slider.component.scss │ │ │ ├── product-slider.component.spec.ts │ │ │ └── product-slider.component.ts │ │ ├── product │ │ │ ├── product.component.html │ │ │ ├── product.component.scss │ │ │ ├── product.component.spec.ts │ │ │ └── product.component.ts │ │ ├── products.module.ts │ │ └── products.routes.ts │ ├── shopping-cart │ │ ├── shopping-cart-container │ │ │ ├── shopping-cart-container.component.html │ │ │ ├── shopping-cart-container.component.scss │ │ │ ├── shopping-cart-container.component.spec.ts │ │ │ └── shopping-cart-container.component.ts │ │ ├── shopping-cart-item │ │ │ ├── shopping-cart-item.component.html │ │ │ ├── shopping-cart-item.component.scss │ │ │ ├── shopping-cart-item.component.spec.ts │ │ │ └── shopping-cart-item.component.ts │ │ └── shopping-cart.module.ts │ ├── store │ │ ├── app.reducer.ts │ │ ├── brand-filter │ │ │ ├── brand-filter.action.ts │ │ │ ├── brand-filter.action.types.ts │ │ │ └── brand-filter.reducer.ts │ │ ├── price-filter │ │ │ ├── price-filter.action.ts │ │ │ ├── price-filter.reducer.ts │ │ │ └── price-filter.types.ts │ │ └── shop │ │ │ ├── shop.action.ts │ │ │ ├── shop.action.types.ts │ │ │ └── shop.reducer.ts │ └── utilities │ │ └── cumulativeOffset.ts ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | 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 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Here is the live demo CLICK TO SEE DEMO 2 | 3 | ![alt-text](https://github.com/TheCodersDream/Angular-Ecommerce-App-with-NGRX/blob/master/angular_ecommerce_img_1.png) 4 | ![alt-text](https://github.com/TheCodersDream/Angular-Ecommerce-App-with-NGRX/blob/master/angular_ecommerce_img_2.png) 5 | ![alt-text](https://github.com/TheCodersDream/Angular-Ecommerce-App-with-NGRX/blob/master/angular_ecommerce_img_3.png) 6 | ![alt-text](https://github.com/TheCodersDream/Angular-Ecommerce-App-with-NGRX/blob/master/angular_ecommerce_img_4.png) 7 | ![alt-text](https://github.com/TheCodersDream/Angular-Ecommerce-App-with-NGRX/blob/master/angular_ecommerce_img_5.png) 8 | 9 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.5. 10 | 11 | ## Development server 12 | 13 | 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. 14 | 15 | ## Code scaffolding 16 | 17 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 18 | 19 | ## Build 20 | 21 | 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. 22 | 23 | ## Running unit tests 24 | 25 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 26 | 27 | ## Running end-to-end tests 28 | 29 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 30 | 31 | ## Further help 32 | 33 | 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). 34 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ecommerce": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ecommerce", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.scss" 31 | ], 32 | "scripts": [], 33 | "es5BrowserSupport": true 34 | }, 35 | "configurations": { 36 | "production": { 37 | "fileReplacements": [ 38 | { 39 | "replace": "src/environments/environment.ts", 40 | "with": "src/environments/environment.prod.ts" 41 | } 42 | ], 43 | "optimization": true, 44 | "outputHashing": "all", 45 | "sourceMap": false, 46 | "extractCss": true, 47 | "namedChunks": false, 48 | "aot": true, 49 | "extractLicenses": true, 50 | "vendorChunk": false, 51 | "buildOptimizer": true, 52 | "budgets": [ 53 | { 54 | "type": "initial", 55 | "maximumWarning": "2mb", 56 | "maximumError": "5mb" 57 | } 58 | ] 59 | } 60 | } 61 | }, 62 | "serve": { 63 | "builder": "@angular-devkit/build-angular:dev-server", 64 | "options": { 65 | "browserTarget": "ecommerce:build" 66 | }, 67 | "configurations": { 68 | "production": { 69 | "browserTarget": "ecommerce:build:production" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "ecommerce:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "styles": [ 87 | "src/styles.scss" 88 | ], 89 | "scripts": [], 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": [ 100 | "src/tsconfig.app.json", 101 | "src/tsconfig.spec.json" 102 | ], 103 | "exclude": [ 104 | "**/node_modules/**" 105 | ] 106 | } 107 | } 108 | } 109 | }, 110 | "ecommerce-e2e": { 111 | "root": "e2e/", 112 | "projectType": "application", 113 | "prefix": "", 114 | "architect": { 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "e2e/protractor.conf.js", 119 | "devServerTarget": "ecommerce:serve" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "devServerTarget": "ecommerce:serve:production" 124 | } 125 | } 126 | }, 127 | "lint": { 128 | "builder": "@angular-devkit/build-angular:tslint", 129 | "options": { 130 | "tsConfig": "e2e/tsconfig.e2e.json", 131 | "exclude": [ 132 | "**/node_modules/**" 133 | ] 134 | } 135 | } 136 | } 137 | } 138 | }, 139 | "defaultProject": "ecommerce" 140 | } -------------------------------------------------------------------------------- /angular_ecommerce_img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/angular_ecommerce_img_1.png -------------------------------------------------------------------------------- /angular_ecommerce_img_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/angular_ecommerce_img_2.png -------------------------------------------------------------------------------- /angular_ecommerce_img_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/angular_ecommerce_img_3.png -------------------------------------------------------------------------------- /angular_ecommerce_img_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/angular_ecommerce_img_4.png -------------------------------------------------------------------------------- /angular_ecommerce_img_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/angular_ecommerce_img_5.png -------------------------------------------------------------------------------- /e2e/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 | './src/**/*.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 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to ecommerce!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecommerce", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.2.0", 15 | "@angular/common": "~7.2.0", 16 | "@angular/compiler": "~7.2.0", 17 | "@angular/core": "~7.2.0", 18 | "@angular/forms": "~7.2.0", 19 | "@angular/platform-browser": "~7.2.0", 20 | "@angular/platform-browser-dynamic": "~7.2.0", 21 | "@angular/router": "~7.2.0", 22 | "@ngrx/store": "^7.3.0", 23 | "@ngrx/store-devtools": "^3.2.4", 24 | "bootstrap": "^4.3.1", 25 | "core-js": "^2.5.4", 26 | "rxjs": "~6.3.3", 27 | "tslib": "^1.9.0", 28 | "zone.js": "~0.8.26" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "~0.13.0", 32 | "@angular/cli": "~7.3.5", 33 | "@angular/compiler-cli": "~7.2.0", 34 | "@angular/language-service": "~7.2.0", 35 | "@types/node": "~8.9.4", 36 | "@types/jasmine": "~2.8.8", 37 | "@types/jasminewd2": "~2.0.3", 38 | "codelyzer": "~4.5.0", 39 | "jasmine-core": "~2.99.1", 40 | "jasmine-spec-reporter": "~4.2.1", 41 | "karma": "~4.0.0", 42 | "karma-chrome-launcher": "~2.2.0", 43 | "karma-coverage-istanbul-reporter": "~2.0.1", 44 | "karma-jasmine": "~1.1.2", 45 | "karma-jasmine-html-reporter": "^0.2.2", 46 | "protractor": "~5.4.0", 47 | "ts-node": "~7.0.0", 48 | "tslint": "~5.11.0", 49 | "typescript": "~3.2.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'ecommerce'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('ecommerce'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to ecommerce!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'ecommerce'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {CoreModule} from './core/core.module'; 7 | import {ProductsModule} from './products/products.module'; 8 | import {ShoppingCartModule} from './shopping-cart/shopping-cart.module'; 9 | import {PaginationModule} from './pagination/pagination.module'; 10 | 11 | import {StoreModule} from '@ngrx/store'; 12 | import {reducers} from './store/app.reducer'; 13 | import {RouterModule} from '@angular/router'; 14 | import {appRoutes} from './app.routes'; 15 | import {PagesModule} from './pages/pages.module'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | AppComponent 20 | ], 21 | imports: [ 22 | BrowserModule, 23 | RouterModule.forRoot(appRoutes), 24 | BrowserAnimationsModule, 25 | CoreModule, 26 | ProductsModule, 27 | ShoppingCartModule, 28 | PaginationModule, 29 | PagesModule, 30 | StoreModule.forRoot(reducers), 31 | ], 32 | providers: [], 33 | bootstrap: [AppComponent] 34 | }) 35 | export class AppModule { } 36 | 37 | -------------------------------------------------------------------------------- /src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import {Routes} from '@angular/router'; 2 | import {NotFoundComponent} from './core/not-found/not-found.component'; 3 | 4 | export const appRoutes: Routes = [ 5 | {path: '', pathMatch: 'full', redirectTo: 'products'}, 6 | {path: '**', component: NotFoundComponent} 7 | ]; 8 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HeaderComponent } from './header/header.component'; 4 | import { FooterComponent } from './footer/footer.component'; 5 | import { BrandPipe } from './pipes/brand.pipe'; 6 | import { OrderByPricePipe } from './pipes/order-by-price.pipe'; 7 | import { PriceFormatterPipe } from './pipes/price-formatter.pipe'; 8 | import { ShortenTitlePipe } from './pipes/shorten-title.pipe'; 9 | import { LayoutModeComponent } from './layout-mode/layout-mode.component'; 10 | import { NotFoundComponent } from './not-found/not-found.component'; 11 | import {RouterModule} from '@angular/router'; 12 | 13 | @NgModule({ 14 | declarations: [HeaderComponent, FooterComponent, BrandPipe, OrderByPricePipe, PriceFormatterPipe, ShortenTitlePipe, LayoutModeComponent, NotFoundComponent], 15 | imports: [ 16 | CommonModule, 17 | RouterModule 18 | ], 19 | exports: [ 20 | HeaderComponent, FooterComponent, BrandPipe, OrderByPricePipe, PriceFormatterPipe, ShortenTitlePipe, LayoutModeComponent 21 | ] 22 | }) 23 | export class CoreModule { } 24 | -------------------------------------------------------------------------------- /src/app/core/data/brands.ts: -------------------------------------------------------------------------------- 1 | export const brands = ["apple","huawei","meizu","samsung","vestel","xiaomi","asus"]; 2 | -------------------------------------------------------------------------------- /src/app/core/data/phones.ts: -------------------------------------------------------------------------------- 1 | export const phones = [{ 2 | title: 'Apple iPhone 7 Plus 32 GB (Apple Türkiye Garantili)', 3 | category: 'phone', 4 | images: ['https://productimages.hepsiburada.net/s/18/280-413/9801258663986.jpg?v1', 'https://productimages.hepsiburada.net/s/18/280-413/9801258696754.jpg?v1', 'https://productimages.hepsiburada.net/s/18/280-413/9801258729522.jpg?v1', 'https://productimages.hepsiburada.net/s/18/280-413/9801258762290.jpg?v1'], 5 | brand: 'apple', 6 | price: 4241.499828399639, 7 | cpu: '1.3GHz Apple A6', 8 | camera: '8mp (3264x2448)', 9 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 10 | weight: '132 grams (4.7 ounces) with battery', 11 | display: '4.0 326 pixel density', 12 | battery: '1480 mAh', 13 | memory: '16GB, 32GB and RAM 1 GB', 14 | id: 0, 15 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 16 | }, { 17 | title: 'Huawei Mate 20 Lite 64 GB (Huawei Türkiye Garantili)', 18 | category: 'phone', 19 | images: ['https://productimages.hepsiburada.net/s/21/280-413/9933217792050.jpg?v1', 'https://productimages.hepsiburada.net/s/21/280-413/9933217628210.jpg?v1', 'https://productimages.hepsiburada.net/s/21/280-413/9933217660978.jpg?v1', 'https://productimages.hepsiburada.net/s/21/280-413/9933217693746.jpg?v1'], 20 | brand: 'huawei', 21 | price: 1823.6625483451157, 22 | cpu: '1.3GHz Apple A6', 23 | camera: '8mp (3264x2448)', 24 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 25 | weight: '132 grams (4.7 ounces) with battery', 26 | display: '4.0 326 pixel density', 27 | battery: '1480 mAh', 28 | memory: '16GB, 32GB and RAM 1 GB', 29 | id: 1, 30 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 31 | }, { 32 | title: 'Huawei P20 Lite 64 GB (Huawei Türkiye Garantili)', 33 | category: 'phone', 34 | images: ['https://productimages.hepsiburada.net/s/19/280-413/9826975907890.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9826975940658.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9826975973426.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9826976006194.jpg?v1'], 35 | brand: 'huawei', 36 | price: 7429.467511354926, 37 | cpu: '1.3GHz Apple A6', 38 | camera: '8mp (3264x2448)', 39 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 40 | weight: '132 grams (4.7 ounces) with battery', 41 | display: '4.0 326 pixel density', 42 | battery: '1480 mAh', 43 | memory: '16GB, 32GB and RAM 1 GB', 44 | id: 2, 45 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 46 | }, { 47 | title: 'Meizu 16TH 64 GB (Meizu Türkiye Garantili)', 48 | category: 'phone', 49 | images: ['https://productimages.hepsiburada.net/s/24/280-413/10094991409202.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10094991441970.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10094991474738.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10094991507506.jpg?v1'], 50 | brand: 'meizu', 51 | price: 5664.265944453384, 52 | cpu: '1.3GHz Apple A6', 53 | camera: '8mp (3264x2448)', 54 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 55 | weight: '132 grams (4.7 ounces) with battery', 56 | display: '4.0 326 pixel density', 57 | battery: '1480 mAh', 58 | memory: '16GB, 32GB and RAM 1 GB', 59 | id: 3, 60 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 61 | }, { 62 | title: 'Meizu X8 64 GB (Meizu Türkiye Garantili)', 63 | category: 'phone', 64 | images: ['https://productimages.hepsiburada.net/s/25/280-413/10108030091314.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082391818290.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082391851058.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082391883826.jpg?v1'], 65 | brand: 'meizu', 66 | price: 4596.99884783711, 67 | cpu: '1.3GHz Apple A6', 68 | camera: '8mp (3264x2448)', 69 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 70 | weight: '132 grams (4.7 ounces) with battery', 71 | display: '4.0 326 pixel density', 72 | battery: '1480 mAh', 73 | memory: '16GB, 32GB and RAM 1 GB', 74 | id: 4, 75 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 76 | }, { 77 | title: 'Samsung Galaxy A7 2018 64 GB (Samsung Türkiye Garantili)', 78 | category: 'phone', 79 | images: ['https://productimages.hepsiburada.net/s/22/280-413/9946187399218.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9946187431986.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9946187464754.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9946187497522.jpg?v1'], 80 | brand: 'samsung', 81 | price: 4108.082941215698, 82 | cpu: '1.3GHz Apple A6', 83 | camera: '8mp (3264x2448)', 84 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 85 | weight: '132 grams (4.7 ounces) with battery', 86 | display: '4.0 326 pixel density', 87 | battery: '1480 mAh', 88 | memory: '16GB, 32GB and RAM 1 GB', 89 | id: 5, 90 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 91 | }, { 92 | title: 'Samsung Galaxy J6 Plus 32 GB (Samsung Türkiye Garantili)', 93 | category: 'phone', 94 | images: ['https://productimages.hepsiburada.net/s/22/280-413/9941129494578.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9941129527346.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9941129560114.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9941129592882.jpg?v1'], 95 | brand: 'samsung', 96 | price: 4260.9529075338505, 97 | cpu: '1.3GHz Apple A6', 98 | camera: '8mp (3264x2448)', 99 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 100 | weight: '132 grams (4.7 ounces) with battery', 101 | display: '4.0 326 pixel density', 102 | battery: '1480 mAh', 103 | memory: '16GB, 32GB and RAM 1 GB', 104 | id: 6, 105 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 106 | }, { 107 | title: 'Vestel Venus Z20 (Vestel Garantili)', 108 | category: 'phone', 109 | images: ['https://productimages.hepsiburada.net/s/19/280-413/9841497047090.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9841497079858.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9841497112626.jpg?v1', 'https://productimages.hepsiburada.net/s/19/280-413/9841497145394.jpg?v1'], 110 | brand: 'vestel', 111 | price: 4730.962860489047, 112 | cpu: '1.3GHz Apple A6', 113 | camera: '8mp (3264x2448)', 114 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 115 | weight: '132 grams (4.7 ounces) with battery', 116 | display: '4.0 326 pixel density', 117 | battery: '1480 mAh', 118 | memory: '16GB, 32GB and RAM 1 GB', 119 | id: 7, 120 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 121 | }, { 122 | title: 'Xiaomi Mi 8 Lite 128 GB (İthalatçı Garantili)', 123 | category: 'phone', 124 | images: ['https://productimages.hepsiburada.net/s/22/280-413/9957349523506.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9957349556274.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9957349589042.jpg?v1', 'https://productimages.hepsiburada.net/s/22/280-413/9957349621810.jpg?v1'], 125 | brand: 'xiaomi', 126 | price: 5565.737301732921, 127 | cpu: '1.3GHz Apple A6', 128 | camera: '8mp (3264x2448)', 129 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 130 | weight: '132 grams (4.7 ounces) with battery', 131 | display: '4.0 326 pixel density', 132 | battery: '1480 mAh', 133 | memory: '16GB, 32GB and RAM 1 GB', 134 | id: 8, 135 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 136 | }, { 137 | title: 'Xiaomi Mi 8 Lite 64 GB (İthalatçı Garantili)', 138 | category: 'phone', 139 | images: ['https://productimages.hepsiburada.net/s/23/280-413/10051147202610.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10051147235378.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10051147268146.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10051147300914.jpg?v1'], 140 | brand: 'xiaomi', 141 | price: 5830.067673371856, 142 | cpu: '1.3GHz Apple A6', 143 | camera: '8mp (3264x2448)', 144 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 145 | weight: '132 grams (4.7 ounces) with battery', 146 | display: '4.0 326 pixel density', 147 | battery: '1480 mAh', 148 | memory: '16GB, 32GB and RAM 1 GB', 149 | id: 9, 150 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 151 | }, { 152 | title: 'Apple iPhone 7 32 GB (Apple Türkiye Garantili)', 153 | category: 'phone', 154 | images: ['https://productimages.hepsiburada.net/s/1/280-413/9502147641394.jpg?v1', 'https://productimages.hepsiburada.net/s/1/280-413/9502147674162.jpg?v1', 'https://productimages.hepsiburada.net/s/1/280-413/9502147706930.jpg?v1', 'https://productimages.hepsiburada.net/s/1/280-413/9502147739698.jpg?v1'], 155 | brand: 'apple', 156 | price: 1525.6236967422828, 157 | cpu: '1.3GHz Apple A6', 158 | camera: '8mp (3264x2448)', 159 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 160 | weight: '132 grams (4.7 ounces) with battery', 161 | display: '4.0 326 pixel density', 162 | battery: '1480 mAh', 163 | memory: '16GB, 32GB and RAM 1 GB', 164 | id: 10, 165 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 166 | }, { 167 | title: 'Samsung Galaxy S10 Plus 128 GB (Samsung Türkiye Garantili)', 168 | category: 'phone', 169 | images: ['https://productimages.hepsiburada.net/s/25/280-413/10107992703026.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107992735794.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107992768562.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107992801330.jpg?v1'], 170 | brand: 'samsung', 171 | price: 3429.3471420603028, 172 | cpu: '1.3GHz Apple A6', 173 | camera: '8mp (3264x2448)', 174 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 175 | weight: '132 grams (4.7 ounces) with battery', 176 | display: '4.0 326 pixel density', 177 | battery: '1480 mAh', 178 | memory: '16GB, 32GB and RAM 1 GB', 179 | id: 11, 180 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 181 | }, { 182 | title: 'Samsung Galaxy S10 128 GB (Samsung Türkiye Garantili)', 183 | category: 'phone', 184 | images: ['https://productimages.hepsiburada.net/s/25/280-413/10107307425842.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307458610.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307491378.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307524146.jpg?v1'], 185 | brand: 'samsung', 186 | price: 1017.7877018963508, 187 | cpu: '1.3GHz Apple A6', 188 | camera: '8mp (3264x2448)', 189 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 190 | weight: '132 grams (4.7 ounces) with battery', 191 | display: '4.0 326 pixel density', 192 | battery: '1480 mAh', 193 | memory: '16GB, 32GB and RAM 1 GB', 194 | id: 12, 195 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 196 | }, { 197 | title: 'Samsung Galaxy S10e 128 GB (Samsung Türkiye Garantili)', 198 | category: 'phone', 199 | images: ['https://productimages.hepsiburada.net/s/25/280-413/10107307032626.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307065394.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307098162.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10107307130930.jpg?v1'], 200 | brand: 'samsung', 201 | price: 3235.202225041739, 202 | cpu: '1.3GHz Apple A6', 203 | camera: '8mp (3264x2448)', 204 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 205 | weight: '132 grams (4.7 ounces) with battery', 206 | display: '4.0 326 pixel density', 207 | battery: '1480 mAh', 208 | memory: '16GB, 32GB and RAM 1 GB', 209 | id: 13, 210 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 211 | }, { 212 | title: 'Samsung Galaxy M20 32 GB (Samsung Türkiye Garantili)', 213 | category: 'phone', 214 | images: ['https://productimages.hepsiburada.net/s/25/280-413/10094999240754.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10094999273522.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10094999306290.jpg?v1', 'https://productimages.hepsiburada.net/s/25/280-413/10094999339058.jpg?v1'], 215 | brand: 'samsung', 216 | price: 5850.5748675199875, 217 | cpu: '1.3GHz Apple A6', 218 | camera: '8mp (3264x2448)', 219 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 220 | weight: '132 grams (4.7 ounces) with battery', 221 | display: '4.0 326 pixel density', 222 | battery: '1480 mAh', 223 | memory: '16GB, 32GB and RAM 1 GB', 224 | id: 14, 225 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 226 | }, { 227 | title: 'Samsung Galaxy S8 (Samsung Türkiye Garantili)', 228 | category: 'phone', 229 | images: ['https://productimages.hepsiburada.net/s/3/280-413/9604775739442.jpg?v1', 'https://productimages.hepsiburada.net/s/4/280-413/9665566703666.jpg?v1', 'https://productimages.hepsiburada.net/s/4/280-413/9665566736434.jpg?v1', 'https://productimages.hepsiburada.net/s/4/280-413/9665566769202.jpg?v1'], 230 | brand: 'samsung', 231 | price: 3207.2840718201587, 232 | cpu: '1.3GHz Apple A6', 233 | camera: '8mp (3264x2448)', 234 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 235 | weight: '132 grams (4.7 ounces) with battery', 236 | display: '4.0 326 pixel density', 237 | battery: '1480 mAh', 238 | memory: '16GB, 32GB and RAM 1 GB', 239 | id: 15, 240 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 241 | }, { 242 | title: 'Huawei P Smart 2019 64 GB (Huawei Türkiye Garantili)', 243 | category: 'phone', 244 | images: ['https://productimages.hepsiburada.net/s/23/280-413/10059934859314.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10059934892082.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10059934924850.jpg?v1', 'https://productimages.hepsiburada.net/s/23/280-413/10059934957618.jpg?v1'], 245 | brand: 'huawei', 246 | price: 5288.552334214134, 247 | cpu: '1.3GHz Apple A6', 248 | camera: '8mp (3264x2448)', 249 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 250 | weight: '132 grams (4.7 ounces) with battery', 251 | display: '4.0 326 pixel density', 252 | battery: '1480 mAh', 253 | memory: '16GB, 32GB and RAM 1 GB', 254 | id: 16, 255 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 256 | }, { 257 | title: 'Xiaomi Mi 8 128 GB (İthalatçı Garantili)', 258 | category: 'phone', 259 | images: ['https://productimages.hepsiburada.net/s/20/280-413/9873948540978.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9873948573746.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9873948606514.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9873948639282.jpg?v1'], 260 | brand: 'xiaomi', 261 | price: 1354.662450097338, 262 | cpu: '1.3GHz Apple A6', 263 | camera: '8mp (3264x2448)', 264 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 265 | weight: '132 grams (4.7 ounces) with battery', 266 | display: '4.0 326 pixel density', 267 | battery: '1480 mAh', 268 | memory: '16GB, 32GB and RAM 1 GB', 269 | id: 17, 270 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 271 | }, { 272 | title: 'Apple iPhone 6S Plus 32 GB (Apple Türkiye Garantili)', 273 | category: 'phone', 274 | images: ['https://productimages.hepsiburada.net/s/1/280-413/9489589993522.jpg?v1', 'https://productimages.hepsiburada.net/s/1/280-413/9489589927986.jpg?v1', 'https://productimages.hepsiburada.net/s/1/280-413/9489589960754.jpg?v1'], 275 | brand: 'apple', 276 | price: 5467.082548922358, 277 | cpu: '1.3GHz Apple A6', 278 | camera: '8mp (3264x2448)', 279 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 280 | weight: '132 grams (4.7 ounces) with battery', 281 | display: '4.0 326 pixel density', 282 | battery: '1480 mAh', 283 | memory: '16GB, 32GB and RAM 1 GB', 284 | id: 18, 285 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 286 | }, { 287 | title: 'Meizu 16 64 GB (Meizu Türkiye Garantili)', 288 | category: 'phone', 289 | images: ['https://productimages.hepsiburada.net/s/24/280-413/10082399387698.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082399420466.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082399453234.jpg?v1', 'https://productimages.hepsiburada.net/s/24/280-413/10082399486002.jpg?v1'], 290 | brand: 'meizu', 291 | price: 1173.3514841571446, 292 | cpu: '1.3GHz Apple A6', 293 | camera: '8mp (3264x2448)', 294 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 295 | weight: '132 grams (4.7 ounces) with battery', 296 | display: '4.0 326 pixel density', 297 | battery: '1480 mAh', 298 | memory: '16GB, 32GB and RAM 1 GB', 299 | id: 19, 300 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 301 | }, { 302 | title: 'Vestel Venüs E3 (Vestel Türkiye Garantili)', 303 | category: 'phone', 304 | images: ['https://productimages.hepsiburada.net/s/20/280-413/9885707108402.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9885707141170.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9885707173938.jpg?v1'], 305 | brand: 'vestel', 306 | price: 2371.8351283872053, 307 | cpu: '1.3GHz Apple A6', 308 | camera: '8mp (3264x2448)', 309 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 310 | weight: '132 grams (4.7 ounces) with battery', 311 | display: '4.0 326 pixel density', 312 | battery: '1480 mAh', 313 | memory: '16GB, 32GB and RAM 1 GB', 314 | id: 20, 315 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 316 | }, { 317 | title: 'Asus Zenfone Max Pro ZB602KL 64 GB (Asus Türkiye Garantili)', 318 | category: 'phone', 319 | images: ['https://productimages.hepsiburada.net/s/21/280-413/9924255121458.jpg?v1', 'https://productimages.hepsiburada.net/s/21/280-413/9924255154226.jpg?v1', 'https://productimages.hepsiburada.net/s/21/280-413/9924255186994.jpg?v1'], 320 | brand: 'asus', 321 | price: 2603.504706144322, 322 | cpu: '1.3GHz Apple A6', 323 | camera: '8mp (3264x2448)', 324 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 325 | weight: '132 grams (4.7 ounces) with battery', 326 | display: '4.0 326 pixel density', 327 | battery: '1480 mAh', 328 | memory: '16GB, 32GB and RAM 1 GB', 329 | id: 21, 330 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 331 | }, { 332 | title: 'Samsung Galaxy Note 9 128 GB (Samsung Türkiye Garantili)', 333 | category: 'phone', 334 | images: ['https://productimages.hepsiburada.net/s/20/280-413/9902572142642.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9902572175410.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9902572208178.jpg?v1', 'https://productimages.hepsiburada.net/s/20/280-413/9902572240946.jpg?v1'], 335 | brand: 'samsung', 336 | price: 1165.0255199945123, 337 | cpu: '1.3GHz Apple A6', 338 | camera: '8mp (3264x2448)', 339 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 340 | weight: '132 grams (4.7 ounces) with battery', 341 | display: '4.0 326 pixel density', 342 | battery: '1480 mAh', 343 | memory: '16GB, 32GB and RAM 1 GB', 344 | id: 22, 345 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 346 | }, { 347 | title: 'Huawei Mate 20 Lite Dual Sim 64 GB (İthalatçı Garantili)', 348 | category: 'phone', 349 | images: ['https://productimages.hepsiburada.net/s/21/280-413/9933217792050.jpg?v1', 350 | 'https://productimages.hepsiburada.net/s/21/280-413/9933217660978.jpg?v1', 351 | 'https://productimages.hepsiburada.net/s/21/280-413/9933217693746.jpg?v1', 352 | 'https://productimages.hepsiburada.net/s/21/280-413/9933217726514.jpg?v1'], 353 | brand: 'huawei', 354 | price: 2693.4407990587074, 355 | cpu: '1.3GHz Apple A6', 356 | camera: '8mp (3264x2448)', 357 | size: '124.4mm x 59.2mm x 8.97mm (4.9 x 2.33 x 0.35)', 358 | weight: '132 grams (4.7 ounces) with battery', 359 | display: '4.0 326 pixel density', 360 | battery: '1480 mAh', 361 | memory: '16GB, 32GB and RAM 1 GB', 362 | id: 23, 363 | description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Amet numquam aspernatur!' 364 | }]; 365 | 366 | -------------------------------------------------------------------------------- /src/app/core/enums/event-types.ts: -------------------------------------------------------------------------------- 1 | export enum EventTypes { 2 | MOUSE_MOVE= 'mousemove', 3 | TOUCH_MOVE = 'touchmove' 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/enums/order.ts: -------------------------------------------------------------------------------- 1 | export enum Order { 2 | ASC = 'asc', 3 | DESC = 'desc' 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Copyright © Emre Baskan 2019

4 |
5 |
6 | -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/core/footer/footer.component.scss -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FooterComponent } from './footer.component'; 4 | 5 | describe('FooterComponent', () => { 6 | let component: FooterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ FooterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FooterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/header/header.component.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/app/core/header/header.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/core/header/header.component.scss -------------------------------------------------------------------------------- /src/app/core/header/header.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HeaderComponent } from './header.component'; 4 | 5 | describe('HeaderComponent', () => { 6 | let component: HeaderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HeaderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HeaderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import {Observable} from 'rxjs'; 4 | import * as fromApp from '../../store/app.reducer'; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'] 10 | }) 11 | export class HeaderComponent implements OnInit { 12 | 13 | shop: Observable; 14 | constructor(private store: Store) { } 15 | 16 | ngOnInit() { 17 | this.shop = this.store.select('shop'); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/layout-mode/layout-mode.component.html: -------------------------------------------------------------------------------- 1 |
2 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/core/layout-mode/layout-mode.component.scss: -------------------------------------------------------------------------------- 1 | .layout-mode { 2 | 3 | display: flex; 4 | flex-direction: row; 5 | padding: .3rem; 6 | border: 2px solid gray; 7 | cursor: pointer; 8 | 9 | &__item { 10 | width: .7rem; 11 | height: .7rem; 12 | background-color: gray; 13 | 14 | &--active { 15 | background-color: dodgerblue; 16 | } 17 | 18 | } 19 | 20 | &__item:not(:last-child) { 21 | margin-right: .3rem; 22 | } 23 | 24 | 25 | &:hover { 26 | border: 2px solid #646464; 27 | } 28 | 29 | &--active { 30 | border: 2px solid dodgerblue; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/core/layout-mode/layout-mode.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LayoutModeComponent } from './layout-mode.component'; 4 | 5 | describe('LayoutModeComponent', () => { 6 | let component: LayoutModeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ LayoutModeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LayoutModeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/layout-mode/layout-mode.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-layout-mode', 5 | templateUrl: './layout-mode.component.html', 6 | styleUrls: ['./layout-mode.component.scss'] 7 | }) 8 | export class LayoutModeComponent implements OnInit { 9 | 10 | @Input('isActive') isActive: boolean; 11 | @Input('len') len: number; 12 | @Output('changeLayout') changeLayout = new EventEmitter(); 13 | 14 | constructor() { 15 | } 16 | 17 | items = []; 18 | 19 | ngOnInit() { 20 | for (let i = 0; i < this.len; i++) { 21 | this.items.push(i); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/app/core/models/Product.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | title: string; 3 | category: string; 4 | images: string[]; 5 | brand: string; 6 | price: number; 7 | cpu: string; 8 | camera: string; 9 | size: string; 10 | weight: string; 11 | display: string; 12 | battery: string; 13 | memory: string; 14 | id: number; 15 | quantity?: number; 16 | description: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |

2 | Page not found 3 |

4 | -------------------------------------------------------------------------------- /src/app/core/not-found/not-found.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/core/not-found/not-found.component.scss -------------------------------------------------------------------------------- /src/app/core/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotFoundComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | templateUrl: './not-found.component.html', 6 | styleUrls: ['./not-found.component.scss'] 7 | }) 8 | export class NotFoundComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/pipes/brand.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { BrandPipe } from './brand.pipe'; 2 | 3 | describe('BrandPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new BrandPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/core/pipes/brand.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import {Product} from '../models/Product'; 3 | 4 | @Pipe({ 5 | name: 'brand' 6 | }) 7 | export class BrandPipe implements PipeTransform { 8 | 9 | transform(products: Product[], brand: string): Product[] { 10 | if (!brand) return products; 11 | 12 | return products.filter(product => brand.includes(product.brand)); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/pipes/order-by-price.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { OrderByPricePipe } from './order-by-price.pipe'; 2 | 3 | describe('OrderByPricePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new OrderByPricePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/core/pipes/order-by-price.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Pipe, PipeTransform} from '@angular/core'; 2 | import {Order} from '../enums/order'; 3 | import {Product} from '../models/Product'; 4 | 5 | @Pipe({ 6 | name: 'orderByPrice' 7 | }) 8 | export class OrderByPricePipe implements PipeTransform { 9 | 10 | transform(products: Product[], order?: Order): Product[] { 11 | if (!order) { return products; } 12 | 13 | if (order === Order.ASC) { 14 | return products.slice().sort((el1, el2) => el1.price - el2.price); 15 | } else { 16 | return products.slice().sort((el1, el2) => el2.price - el1.price); 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/core/pipes/price-formatter.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { PriceFormatterPipe } from './price-formatter.pipe'; 2 | 3 | describe('PriceFormatterPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new PriceFormatterPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/core/pipes/price-formatter.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'priceFormatter' 5 | }) 6 | export class PriceFormatterPipe implements PipeTransform { 7 | 8 | transform(price: number): string { 9 | return price.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,'); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/pipes/shorten-title.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ShortenTitlePipe } from './shorten-title.pipe'; 2 | 3 | describe('ShortenTitlePipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new ShortenTitlePipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/core/pipes/shorten-title.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'shortenTitle' 5 | }) 6 | export class ShortenTitlePipe implements PipeTransform { 7 | 8 | transform(title: string): string { 9 | return title.split(' (')[0]; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/pure-pipes/brand.pure.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Product} from '../models/Product'; 2 | 3 | export const brandPurePipe = (products: Product[], brand: string): Product[] => { 4 | if (!brand) return products; 5 | return products.filter(product => brand.includes(product.brand)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/core/pure-pipes/order-by-price.pure.pipe.ts: -------------------------------------------------------------------------------- 1 | import {Product} from '../models/Product'; 2 | import {Order} from '../enums/order'; 3 | 4 | export const orderByPricePurePipe = (products: Product[], order?: Order): Product[] => { 5 | if (!order) { return products; } 6 | 7 | if (order === Order.ASC) { 8 | return products.slice().sort((el1, el2) => el1.price - el2.price); 9 | } else { 10 | return products.slice().sort((el1, el2) => el2.price - el1.price); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/filters/brand-filter/brand-filter.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Brands

4 |
5 |
    6 |
  • 7 | 13 |
  • 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/app/filters/brand-filter/brand-filter.component.scss: -------------------------------------------------------------------------------- 1 | .custom-checkbox { 2 | display: block; 3 | position: relative; 4 | padding-left: 2rem; 5 | cursor: pointer; 6 | user-select: none; 7 | 8 | &__input { 9 | position: absolute; 10 | opacity: 0; 11 | height: 0; 12 | width: 0; 13 | 14 | 15 | } 16 | 17 | &__span { 18 | position: absolute; 19 | top: 50%; 20 | left: 0; 21 | transform: translateY(-50%); 22 | height: 1.2rem; 23 | width: 1.2rem; 24 | background-color: white; 25 | border: 2px solid gray; 26 | 27 | &::after { 28 | content: ""; 29 | opacity: 0; 30 | position: absolute; 31 | top: 50%; 32 | left: 50%; 33 | width: .6rem; 34 | height: .6rem; 35 | background-color: dodgerblue; 36 | transform: translate(-50%, -50%); 37 | } 38 | } 39 | 40 | &:hover &__span { 41 | background-color: #eeeeee; 42 | } 43 | 44 | &__input:checked + &__span::after { 45 | opacity: 1; 46 | } 47 | } 48 | 49 | .flex-50 { 50 | flex: 0 0 100%; 51 | } 52 | 53 | @media only screen and (max-width: 768px) { 54 | 55 | .flex-50 { 56 | flex: 0 0 50%; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/filters/brand-filter/brand-filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BrandFilterComponent } from './brand-filter.component'; 4 | 5 | describe('BrandFilterComponent', () => { 6 | let component: BrandFilterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ BrandFilterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BrandFilterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/filters/brand-filter/brand-filter.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {brands} from '../../core/data/brands'; 3 | import {Store} from '@ngrx/store'; 4 | import * as fromApp from '../../store/app.reducer'; 5 | import {AddBrandToFilter, RemoveBrandFromFilter} from '../../store/brand-filter/brand-filter.action'; 6 | 7 | @Component({ 8 | selector: 'app-brand-filter', 9 | templateUrl: './brand-filter.component.html', 10 | styleUrls: ['./brand-filter.component.scss'] 11 | }) 12 | export class BrandFilterComponent implements OnInit { 13 | 14 | 15 | brands = brands; 16 | brandItemsCount; 17 | 18 | constructor(private store: Store) { 19 | } 20 | 21 | ngOnInit() { 22 | this.store.select('shop').subscribe(shop => { 23 | const counts = {}; 24 | shop.products.forEach(p => { 25 | counts[p.brand] = counts[p.brand] + 1 || 1; 26 | }); 27 | this.brandItemsCount = counts; 28 | }); 29 | } 30 | 31 | onChangeSelectBox(e): void { 32 | const name = e.target.name; 33 | const value = e.target.checked; 34 | 35 | if (value) { 36 | this.store.dispatch(new AddBrandToFilter(name)); 37 | } else { 38 | this.store.dispatch(new RemoveBrandFromFilter(name)); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/filters/filters.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BrandFilterComponent } from './brand-filter/brand-filter.component'; 4 | import { PriceFilterComponent } from './price-filter/price-filter.component'; 5 | 6 | @NgModule({ 7 | declarations: [BrandFilterComponent, PriceFilterComponent], 8 | imports: [ 9 | CommonModule 10 | ], 11 | exports: [BrandFilterComponent, PriceFilterComponent] 12 | }) 13 | export class FiltersModule { } 14 | -------------------------------------------------------------------------------- /src/app/filters/price-filter/price-filter.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Price

4 |
5 |
    6 |
  • 7 | 15 |
  • 16 |
  • 17 | 24 |
  • 25 |
26 |
27 | -------------------------------------------------------------------------------- /src/app/filters/price-filter/price-filter.component.scss: -------------------------------------------------------------------------------- 1 | .custom-radio-btn { 2 | display: block; 3 | position: relative; 4 | cursor: pointer; 5 | padding-left: 2rem; 6 | user-select: none; 7 | 8 | &__input { 9 | position: absolute; 10 | opacity: 0; 11 | cursor: pointer; 12 | } 13 | 14 | &__span { 15 | position: absolute; 16 | top: 50%; 17 | left: 0; 18 | height: 1.3rem; 19 | width: 1.3rem; 20 | background-color: #eeeeee; 21 | border-radius: 12px; 22 | transform: translateY(-50%); 23 | 24 | &::after { 25 | position: absolute; 26 | top: 50%; 27 | left: 50%; 28 | transform: translate(-50%, -50%); 29 | width: .7rem; 30 | height: .7rem; 31 | border-radius: 50%; 32 | background-color: dodgerblue; 33 | content: ""; 34 | display: none; 35 | } 36 | } 37 | 38 | &__input:checked ~ &__span::after { 39 | display: block; 40 | } 41 | 42 | &:hover &__span { 43 | background-color: #dfdfdf; 44 | } 45 | 46 | 47 | } 48 | 49 | .text-remove-selected { 50 | font-size: 1rem; 51 | font-weight: normal; 52 | cursor: pointer; 53 | padding-left: 2rem; 54 | } 55 | -------------------------------------------------------------------------------- /src/app/filters/price-filter/price-filter.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PriceFilterComponent } from './price-filter.component'; 4 | 5 | describe('PriceFilterComponent', () => { 6 | let component: PriceFilterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PriceFilterComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PriceFilterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/filters/price-filter/price-filter.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import * as fromApp from '../../store/app.reducer'; 4 | import {ClearOrderBy, OrderByAsc, OrderByDesc} from '../../store/price-filter/price-filter.action'; 5 | import {Order} from '../../core/enums/order'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-price-filter', 10 | templateUrl: './price-filter.component.html', 11 | styleUrls: ['./price-filter.component.scss'] 12 | }) 13 | export class PriceFilterComponent implements OnInit { 14 | 15 | selectedFilter = ''; 16 | Order = Order; 17 | 18 | constructor(private store: Store) { 19 | } 20 | 21 | ngOnInit() { 22 | } 23 | 24 | onRadioButtonChange(e): void { 25 | const value = e.target.value; 26 | if (value === this.selectedFilter) { 27 | this.removefilters(); 28 | return; 29 | } 30 | 31 | this.selectedFilter = value; 32 | 33 | if (value === Order.ASC) { 34 | this.store.dispatch(new OrderByAsc()); 35 | } else { 36 | this.store.dispatch(new OrderByDesc()); 37 | } 38 | } 39 | 40 | removefilters(): void { 41 | const buttons = document.getElementsByName('orderByPrice') as NodeList; 42 | 43 | buttons.forEach((el: HTMLInputElement) => { 44 | el.checked = false; 45 | }); 46 | this.selectedFilter = ''; 47 | this.store.dispatch(new ClearOrderBy()); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/pages/home/home.component.scss -------------------------------------------------------------------------------- /src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Store} from '@ngrx/store'; 4 | import * as fromApp from '../../store/app.reducer'; 5 | import {ClearBrandFilter, RemoveBrandFromFilter} from '../../store/brand-filter/brand-filter.action'; 6 | import {ClearOrderBy} from '../../store/price-filter/price-filter.action'; 7 | 8 | @Component({ 9 | selector: 'app-home', 10 | templateUrl: './home.component.html', 11 | styleUrls: ['./home.component.scss'] 12 | }) 13 | export class HomeComponent implements OnInit { 14 | 15 | constructor(private store: Store) { } 16 | 17 | ngOnInit() { 18 | this.store.dispatch(new ClearOrderBy()); 19 | this.store.dispatch(new ClearBrandFilter()); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/pages.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HomeComponent } from './home/home.component'; 4 | import { ProductDetailPageComponent } from './product-detail-page/product-detail-page.component'; 5 | import { ShoppingCartPageComponent } from './shopping-cart-page/shopping-cart-page.component'; 6 | import {ProductsModule} from '../products/products.module'; 7 | import {RouterModule} from '@angular/router'; 8 | import {pagesRoute} from './pages.route'; 9 | import {FiltersModule} from '../filters/filters.module'; 10 | import {ShoppingCartModule} from '../shopping-cart/shopping-cart.module'; 11 | 12 | @NgModule({ 13 | declarations: [HomeComponent, ProductDetailPageComponent, ShoppingCartPageComponent], 14 | imports: [ 15 | CommonModule, 16 | ProductsModule, 17 | RouterModule, 18 | FiltersModule, 19 | ShoppingCartModule, 20 | RouterModule.forChild(pagesRoute) 21 | ], 22 | exports: [RouterModule] 23 | }) 24 | export class PagesModule { } 25 | -------------------------------------------------------------------------------- /src/app/pages/pages.route.ts: -------------------------------------------------------------------------------- 1 | import {Routes} from '@angular/router'; 2 | import {HomeComponent} from './home/home.component'; 3 | import {ProductDetailPageComponent} from './product-detail-page/product-detail-page.component'; 4 | import {ShoppingCartPageComponent} from './shopping-cart-page/shopping-cart-page.component'; 5 | 6 | export const pagesRoute: Routes = [ 7 | {path: 'products', component: HomeComponent}, 8 | {path: 'products/:id', component: ProductDetailPageComponent}, 9 | {path: 'cart', component: ShoppingCartPageComponent} 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/pages/product-detail-page/product-detail-page.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /src/app/pages/product-detail-page/product-detail-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/pages/product-detail-page/product-detail-page.component.scss -------------------------------------------------------------------------------- /src/app/pages/product-detail-page/product-detail-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductDetailPageComponent } from './product-detail-page.component'; 4 | 5 | describe('ProductDetailPageComponent', () => { 6 | let component: ProductDetailPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductDetailPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductDetailPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/product-detail-page/product-detail-page.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, DoCheck, OnInit} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Store} from '@ngrx/store'; 4 | import {Product} from '../../core/models/Product'; 5 | import * as fromApp from '../../store/app.reducer'; 6 | 7 | @Component({ 8 | selector: 'app-product-detail-page', 9 | templateUrl: './product-detail-page.component.html', 10 | styleUrls: ['./product-detail-page.component.scss'] 11 | }) 12 | export class ProductDetailPageComponent implements OnInit, DoCheck { 13 | 14 | id: number; 15 | product: Product; 16 | 17 | constructor(private route: ActivatedRoute, private store: Store) { 18 | } 19 | 20 | ngDoCheck() { 21 | console.log(this.product); 22 | } 23 | 24 | ngOnInit() { 25 | console.log(this.route.snapshot.params.id); 26 | this.id = this.route.snapshot.params.id; 27 | 28 | this.store.select('shop') 29 | .subscribe((data) => { 30 | console.log(data); 31 | for (let i = 0; i < data.products.length; i++) { 32 | if (data.products[i].id === +this.id) { 33 | console.log(this); 34 | 35 | this.product = data.products[i]; 36 | break; 37 | } 38 | } 39 | }); 40 | 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/app/pages/shopping-cart-page/shopping-cart-page.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/pages/shopping-cart-page/shopping-cart-page.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/pages/shopping-cart-page/shopping-cart-page.component.scss -------------------------------------------------------------------------------- /src/app/pages/shopping-cart-page/shopping-cart-page.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingCartPageComponent } from './shopping-cart-page.component'; 4 | 5 | describe('ShoppingCartPageComponent', () => { 6 | let component: ShoppingCartPageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingCartPageComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingCartPageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/shopping-cart-page/shopping-cart-page.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-shopping-cart-page', 5 | templateUrl: './shopping-cart-page.component.html', 6 | styleUrls: ['./shopping-cart-page.component.scss'] 7 | }) 8 | export class ShoppingCartPageComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit() { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pagination/pagination.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { PaginationComponent } from './pagination/pagination.component'; 4 | import { PaginationPipe } from './pagination.pipe'; 5 | 6 | @NgModule({ 7 | declarations: [PaginationComponent, PaginationPipe], 8 | imports: [ 9 | CommonModule 10 | ], 11 | exports: [PaginationComponent, PaginationPipe] 12 | }) 13 | export class PaginationModule { } 14 | -------------------------------------------------------------------------------- /src/app/pagination/pagination.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { PaginationPipe } from './pagination.pipe'; 2 | 3 | describe('PaginationPipe', () => { 4 | it('create an instance', () => { 5 | const pipe = new PaginationPipe(); 6 | expect(pipe).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/pagination/pagination.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'pagination' 5 | }) 6 | export class PaginationPipe implements PipeTransform { 7 | 8 | transform(value: Array, args: {currentPage: number, perPage: number}): Array { 9 | if (!args || !args.perPage || !args.currentPage) { 10 | return value; 11 | } 12 | const location = (args.perPage * (args.currentPage - 1)) || 0 ; 13 | return value.slice(location, location + args.perPage); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pagination/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /src/app/pagination/pagination/pagination.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/pagination/pagination/pagination.component.scss -------------------------------------------------------------------------------- /src/app/pagination/pagination/pagination.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PaginationComponent } from './pagination.component'; 4 | 5 | describe('PaginationComponent', () => { 6 | let component: PaginationComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ PaginationComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PaginationComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pagination/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-pagination', 5 | templateUrl: './pagination.component.html', 6 | styleUrls: ['./pagination.component.scss'] 7 | }) 8 | export class PaginationComponent implements OnInit { 9 | 10 | @Input('currentPage') currentPage: number; 11 | @Input('totalItemsCount') totalItemsCount: number; 12 | @Input('perPage') perPage: number; 13 | @Input('pagesToShow') pagesToShow: number; 14 | 15 | @Output() prev = new EventEmitter(); 16 | @Output() next = new EventEmitter(); 17 | @Output() goPage = new EventEmitter(); 18 | 19 | constructor() { 20 | } 21 | 22 | 23 | ngOnInit() { 24 | } 25 | 26 | onPage(n: number): void { 27 | this.goPage.emit(n); 28 | console.log(this.getPages()); 29 | } 30 | 31 | isOnLastPage(): boolean { 32 | // console.log(this.perPage * this.currentPage, this.totalItemsCount); 33 | return this.perPage * this.currentPage >= this.totalItemsCount; 34 | } 35 | 36 | totalPages(): number { 37 | return Math.ceil(this.totalItemsCount / this.perPage) || 0; 38 | } 39 | 40 | getMin(): number { 41 | return ((this.perPage * this.currentPage) - this.perPage) + 1; 42 | } 43 | 44 | getMax(): number { 45 | let max = this.perPage * this.currentPage; 46 | if (max > this.totalItemsCount) { 47 | max = this.totalItemsCount; 48 | } 49 | return max; 50 | } 51 | onPrev(): void { 52 | this.prev.emit(true); 53 | } 54 | 55 | onNext(): void { 56 | this.next.emit(true); 57 | } 58 | 59 | 60 | getPages(): number[] { 61 | const c = Math.ceil(this.totalItemsCount / this.perPage); 62 | const p = this.currentPage || 1; 63 | const pagesToShow = this.pagesToShow || 9; 64 | const pages: number[] = []; 65 | pages.push(p); 66 | const times = pagesToShow - 1; 67 | for (let i = 0; i < times; i++) { 68 | if (pages.length < pagesToShow) { 69 | if (Math.min.apply(null, pages) > 1) { 70 | pages.push(Math.min.apply(null, pages) - 1); 71 | } 72 | } 73 | if (pages.length < pagesToShow) { 74 | if (Math.max.apply(null, pages) < c) { 75 | pages.push(Math.max.apply(null, pages) + 1); 76 | } 77 | } 78 | } 79 | pages.sort((a, b) => a - b); 80 | return pages; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/app/products/product-detail/product-detail.component.html: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /src/app/products/product-detail/product-detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/products/product-detail/product-detail.component.scss -------------------------------------------------------------------------------- /src/app/products/product-detail/product-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductDetailComponent } from './product-detail.component'; 4 | 5 | describe('ProductDetailComponent', () => { 6 | let component: ProductDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-detail/product-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {Product} from '../../core/models/Product'; 3 | import {Store} from '@ngrx/store'; 4 | import * as fromApp from '../../store/app.reducer'; 5 | import {AddProductToCart} from '../../store/shop/shop.action'; 6 | 7 | @Component({ 8 | selector: 'app-product-detail', 9 | templateUrl: './product-detail.component.html', 10 | styleUrls: ['./product-detail.component.scss'] 11 | }) 12 | export class ProductDetailComponent implements OnInit { 13 | 14 | @Input('product') product: Product; 15 | constructor(private store: Store) { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | onAddProductToCart(): void { 21 | this.store.dispatch(new AddProductToCart(this.product)); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | Change Layout: 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |
17 | 20 |
21 | 22 | 23 |
24 |
25 | 34 |
35 |
36 | 37 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/products/product-list/product-list.component.scss -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductListComponent } from './product-list.component'; 4 | 5 | describe('ProductListComponent', () => { 6 | let component: ProductListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductListComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | 4 | import * as fromApp from '../../store/app.reducer'; 5 | import {Product} from '../../core/models/Product'; 6 | import {brandPurePipe} from '../../core/pure-pipes/brand.pure.pipe'; 7 | import {orderByPricePurePipe} from '../../core/pure-pipes/order-by-price.pure.pipe'; 8 | import {Order} from '../../core/enums/order'; 9 | 10 | @Component({ 11 | selector: 'app-product-list', 12 | templateUrl: './product-list.component.html', 13 | styleUrls: ['./product-list.component.scss'] 14 | }) 15 | export class ProductListComponent implements OnInit { 16 | 17 | shop: { products: Product[], cart: Product[] }; 18 | products: Product[]; 19 | 20 | colValue = 'col-lg-4'; 21 | gridValue = 3; 22 | perPage = 12; 23 | currentPage = 1; 24 | pagesToShow = 3; 25 | 26 | constructor(private store: Store) { 27 | } 28 | 29 | ngOnInit() { 30 | this.store.select(state => { 31 | return state; 32 | }).subscribe(state => { 33 | const brands = state.brandFilter; 34 | const orderBy = state.orderBy as Order; 35 | 36 | const filterByBrandArr = brandPurePipe(state.shop.products, brands); 37 | const filterByOrderArr = orderByPricePurePipe(filterByBrandArr, orderBy); 38 | 39 | console.log(state); 40 | 41 | this.products = filterByOrderArr; 42 | console.log(this); 43 | }); 44 | } 45 | 46 | onChangeLayout(n: number): void { 47 | this.gridValue = n; 48 | if (n === 4) { 49 | this.colValue = `col-lg-${3}`; 50 | } else { 51 | this.colValue = `col-lg-${4}`; 52 | } 53 | } 54 | 55 | trackProductById(index: number, product: Product): number { 56 | return product.id; 57 | } 58 | 59 | next() { 60 | this.currentPage++; 61 | } 62 | 63 | prev() { 64 | if (this.currentPage === 1) return; 65 | this.currentPage--; 66 | } 67 | 68 | goToPage(loc: number): void { 69 | this.currentPage = loc; 70 | } 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/app/products/product-slider-dots/product-slider-dots.component.html: -------------------------------------------------------------------------------- 1 |
2 | 8 |
9 | -------------------------------------------------------------------------------- /src/app/products/product-slider-dots/product-slider-dots.component.scss: -------------------------------------------------------------------------------- 1 | .owl-dots { 2 | display: none; 3 | position: absolute; 4 | bottom: -1rem; 5 | left: 0; 6 | 7 | } 8 | 9 | .owl-dots { 10 | width: 100%; 11 | text-align: center; 12 | } 13 | 14 | .owl-dots .active.owl-dot { 15 | background-color: #f28b00; 16 | border-color: #f28b00; 17 | } 18 | 19 | .owl-dots .owl-dot { 20 | display: inline-block; 21 | zoom: 1; 22 | margin: 8px 4px; 23 | border-radius: 10px; 24 | -moz-border-radius: 10px; 25 | -o-border-radius: 10px; 26 | -webkit-border-radius: 10px; 27 | -ms-webkit-radius: 10px; 28 | zoom: 1; 29 | background-color: #fff; 30 | padding: 3px; 31 | border: solid 1px #e9e9e9; 32 | cursor: pointer; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/products/product-slider-dots/product-slider-dots.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductSliderDotsComponent } from './product-slider-dots.component'; 4 | 5 | describe('ProductSliderDotsComponent', () => { 6 | let component: ProductSliderDotsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductSliderDotsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductSliderDotsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-slider-dots/product-slider-dots.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-product-slider-dots', 5 | templateUrl: './product-slider-dots.component.html', 6 | styleUrls: ['./product-slider-dots.component.scss'], 7 | encapsulation: ViewEncapsulation.None 8 | }) 9 | export class ProductSliderDotsComponent implements OnInit { 10 | 11 | @Input('len') len: number; 12 | @Input('activeItem') activeItem: number; 13 | @Output('changeItem') changeItem = new EventEmitter(); 14 | 15 | dots = []; 16 | 17 | constructor() { 18 | } 19 | 20 | ngOnInit() { 21 | for (let i = 0; i < this.len; i++) { 22 | this.dots.push(i); 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/app/products/product-slider/product-slider.component.html: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /src/app/products/product-slider/product-slider.component.scss: -------------------------------------------------------------------------------- 1 | .gallery-wrap .img-big-wrap img { 2 | height: 450px; 3 | width: auto; 4 | display: inline-block; 5 | } 6 | 7 | 8 | 9 | .gallery-wrap .img-small-wrap .item-gallery { 10 | width: 60px; 11 | height: 60px; 12 | border: 1px solid #ddd; 13 | margin: 7px 2px; 14 | display: inline-block; 15 | overflow: hidden; 16 | } 17 | 18 | .gallery-wrap .img-small-wrap { 19 | text-align: center; 20 | } 21 | .gallery-wrap .img-small-wrap img { 22 | max-width: 100%; 23 | max-height: 100%; 24 | object-fit: cover; 25 | border-radius: 4px; 26 | cursor: pointer; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/products/product-slider/product-slider.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductSliderComponent } from './product-slider.component'; 4 | 5 | describe('ProductSliderComponent', () => { 6 | let component: ProductSliderComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductSliderComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductSliderComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product-slider/product-slider.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-product-slider', 5 | templateUrl: './product-slider.component.html', 6 | styleUrls: ['./product-slider.component.scss'] 7 | }) 8 | export class ProductSliderComponent implements OnInit { 9 | 10 | @Input('images') images: string[]; 11 | currentImage: string; 12 | 13 | constructor() { 14 | } 15 | 16 | ngOnInit() { 17 | this.currentImage = this.images[0]; 18 | } 19 | 20 | changeImage(n: number): void { 21 | this.currentImage = this.images[n]; 22 | } 23 | 24 | handleImageChange(): void { 25 | 26 | } 27 | 28 | handleMouseOut(): void { 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/products/product/product.component.html: -------------------------------------------------------------------------------- 1 |
2 | 12 | 16 | 17 |
18 |

19 | {{product.title}} 20 |

21 |
${{product.price | priceFormatter}}
22 |

{{product.description}}

23 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /src/app/products/product/product.component.scss: -------------------------------------------------------------------------------- 1 | .product { 2 | padding-bottom: 2rem; 3 | &__img { 4 | width: 100%; 5 | height: 100%; 6 | 7 | &:focus { 8 | outline: none; 9 | } 10 | } 11 | 12 | &__text { 13 | flex: 0 0 40%; 14 | } 15 | 16 | &__link { 17 | flex: 0 0 60%; 18 | height: 60%; 19 | padding: 1rem; 20 | position: relative; 21 | cursor: pointer; 22 | } 23 | 24 | &__title { 25 | font-size: .8rem; 26 | } 27 | 28 | &__price { 29 | 30 | } 31 | 32 | &__description { 33 | font-size: 0.7rem; 34 | } 35 | 36 | &:hover { 37 | box-shadow: 0 1rem 2rem rgba(0,0,0, .2); 38 | } 39 | 40 | &__add-to-cart { 41 | position: absolute; 42 | bottom: .8rem; 43 | width: 60%; 44 | left: 50%; 45 | opacity: 0; 46 | visibility: hidden; 47 | transform: translateX(-50%); 48 | transition: all .2s; 49 | padding: 3px 12px; 50 | 51 | } 52 | 53 | &:hover &__add-to-cart { 54 | visibility: visible; 55 | opacity: 1; 56 | } 57 | 58 | &:hover .owl-dots { 59 | display: inline-block; 60 | } 61 | 62 | } 63 | 64 | @media only screen and (max-width: 768px) { 65 | .product { 66 | height: auto !important; 67 | &__add-to-cart { 68 | visibility: visible; 69 | opacity: 1; 70 | } 71 | 72 | & .owl-dots { 73 | display: inline-block; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/products/product/product.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ProductComponent } from './product.component'; 4 | 5 | describe('ProductComponent', () => { 6 | let component: ProductComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ProductComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ProductComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/products/product/product.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ElementRef, Input, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; 2 | import {Product} from '../../core/models/Product'; 3 | import {cumulativeOffSet} from '../../utilities/cumulativeOffset'; 4 | import {Store} from '@ngrx/store'; 5 | import {AddProductToCart} from '../../store/shop/shop.action'; 6 | import * as fromApp from '../../store/app.reducer'; 7 | import {EventTypes} from '../../core/enums/event-types'; 8 | 9 | @Component({ 10 | selector: 'app-product', 11 | templateUrl: './product.component.html', 12 | styleUrls: ['./product.component.scss'], 13 | encapsulation: ViewEncapsulation.None 14 | }) 15 | export class ProductComponent implements OnInit { 16 | 17 | @Input('product') product: Product; 18 | @ViewChild('productImageContainer') productImageContainer: ElementRef; 19 | 20 | currentImage: string; 21 | currentImageIndex: number; 22 | productImageContainerClientWidth; 23 | offSetLeft: number; 24 | offSetTop: number; 25 | 26 | constructor(private store: Store) { 27 | } 28 | 29 | 30 | ngOnInit() { 31 | this.currentImage = this.product.images[0]; 32 | this.currentImageIndex = 0; 33 | 34 | } 35 | 36 | onImageChange(e): void { 37 | console.log(e); 38 | const eventType = e.type as EventTypes; 39 | let clientX; 40 | this.calculateOffSetLeftAndTop(eventType); 41 | 42 | if (eventType === EventTypes.TOUCH_MOVE) { 43 | clientX = e.touches[0].clientX; 44 | } else if (eventType === EventTypes.MOUSE_MOVE) { 45 | clientX = e.clientX; 46 | } 47 | 48 | const currentX = clientX - this.offSetLeft; 49 | const imgArrLength = this.product.images.length; 50 | 51 | const part = this.productImageContainerClientWidth / imgArrLength; 52 | 53 | let imgIndex = Math.ceil(currentX / part) - 1; 54 | if (imgIndex < 0) { 55 | imgIndex = 0; 56 | } 57 | 58 | if (imgIndex >= imgArrLength) { 59 | imgIndex = imgArrLength - 1; 60 | } 61 | 62 | this.currentImageIndex = imgIndex; 63 | this.currentImage = this.product.images[imgIndex]; 64 | } 65 | 66 | onImageMouseOut(e: MouseEvent): void { 67 | this.currentImage = this.product.images[0]; 68 | } 69 | 70 | onChangeImage(n: number): void { 71 | this.currentImage = this.product.images[n]; 72 | } 73 | 74 | onAddProductToCart(): void { 75 | this.store.dispatch(new AddProductToCart(this.product)); 76 | } 77 | 78 | 79 | calculateOffSetLeftAndTop(eventType: EventTypes): void { 80 | const offSetTopAndLeft = cumulativeOffSet(this.productImageContainer.nativeElement, eventType); 81 | this.offSetLeft = offSetTopAndLeft.left; 82 | this.offSetTop = offSetTopAndLeft.top; 83 | this.productImageContainerClientWidth = this.productImageContainer.nativeElement.clientWidth; 84 | } 85 | 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/app/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ProductComponent } from './product/product.component'; 4 | import { ProductDetailComponent } from './product-detail/product-detail.component'; 5 | import { ProductSliderComponent } from './product-slider/product-slider.component'; 6 | import { ProductSliderDotsComponent } from './product-slider-dots/product-slider-dots.component'; 7 | import {CoreModule} from '../core/core.module'; 8 | import { ProductListComponent } from './product-list/product-list.component'; 9 | import {PaginationModule} from '../pagination/pagination.module'; 10 | import {RouterModule} from '@angular/router'; 11 | import {BrowserModule} from '@angular/platform-browser'; 12 | 13 | @NgModule({ 14 | declarations: [ProductComponent, ProductDetailComponent, ProductSliderComponent, ProductSliderDotsComponent, ProductListComponent], 15 | imports: [ 16 | CommonModule, 17 | CoreModule, 18 | RouterModule, 19 | PaginationModule, 20 | BrowserModule 21 | ], 22 | exports: [ProductComponent, ProductDetailComponent, ProductSliderComponent, ProductSliderDotsComponent, ProductListComponent] 23 | }) 24 | export class ProductsModule { } 25 | -------------------------------------------------------------------------------- /src/app/products/products.routes.ts: -------------------------------------------------------------------------------- 1 | import {Routes} from '@angular/router'; 2 | import {ProductListComponent} from './product-list/product-list.component'; 3 | import {ProductDetailComponent} from './product-detail/product-detail.component'; 4 | 5 | export const productRoutes: Routes = [ 6 | {path: 'products', component: ProductListComponent}, 7 | {path: 'products/:id', component: ProductDetailComponent} 8 | ]; 9 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-container/shopping-cart-container.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | Shopping cart 6 |
7 |
8 |
9 | 10 | 11 | 12 |

13 | There is no item in cart 14 |

15 |
16 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-container/shopping-cart-container.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/app/shopping-cart/shopping-cart-container/shopping-cart-container.component.scss -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-container/shopping-cart-container.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingCartContainerComponent } from './shopping-cart-container.component'; 4 | 5 | describe('ShoppingCartContainerComponent', () => { 6 | let component: ShoppingCartContainerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingCartContainerComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingCartContainerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-container/shopping-cart-container.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Store} from '@ngrx/store'; 3 | import * as fromApp from '../../store/app.reducer'; 4 | import {Product} from '../../core/models/Product'; 5 | 6 | @Component({ 7 | selector: 'app-shopping-cart-container', 8 | templateUrl: './shopping-cart-container.component.html', 9 | styleUrls: ['./shopping-cart-container.component.scss'] 10 | }) 11 | export class ShoppingCartContainerComponent implements OnInit { 12 | 13 | totalPrice: number; 14 | cart: Product[]; 15 | 16 | constructor(private store: Store) { 17 | } 18 | 19 | ngOnInit() { 20 | this.store.select('shop').subscribe(shop => { 21 | this.totalPrice = shop.cart.reduce((count, curItem) => { 22 | return count + (curItem.quantity * curItem.price); 23 | }, 0); 24 | 25 | this.cart = shop.cart; 26 | 27 | }); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-item/shopping-cart-item.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | prewiew 5 |
6 |
7 |

{{product.title | shortenTitle}}

8 |

9 | {{product.description}} 10 |

11 |
12 |
13 |
14 |
{{product.price | priceFormatter}}$ x
15 |
16 |
17 |
18 | 21 | 27 | 30 |
31 |
32 |
33 | 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-item/shopping-cart-item.component.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .quantity { 4 | float: left; 5 | margin-right: 15px; 6 | background-color: #eee; 7 | position: relative; 8 | width: 80px; 9 | overflow: hidden 10 | } 11 | 12 | .quantity input { 13 | margin: 0; 14 | text-align: center; 15 | width: 15px; 16 | height: 15px; 17 | padding: 0; 18 | float: right; 19 | color: #000; 20 | font-size: 20px; 21 | border: 0; 22 | outline: 0; 23 | background-color: #F6F6F6 24 | } 25 | 26 | .quantity input.qty { 27 | position: relative; 28 | border: 0; 29 | width: 100%; 30 | height: 40px; 31 | padding: 10px 25px 10px 10px; 32 | text-align: center; 33 | font-weight: 400; 34 | font-size: 15px; 35 | border-radius: 0; 36 | background-clip: padding-box 37 | } 38 | 39 | .quantity .minus, .quantity .plus { 40 | line-height: 0; 41 | background-clip: padding-box; 42 | -webkit-border-radius: 0; 43 | -moz-border-radius: 0; 44 | border-radius: 0; 45 | -webkit-background-size: 6px 30px; 46 | -moz-background-size: 6px 30px; 47 | color: #bbb; 48 | font-size: 20px; 49 | position: absolute; 50 | height: 50%; 51 | border: 0; 52 | right: 0; 53 | padding: 0; 54 | width: 25px; 55 | z-index: 3 56 | } 57 | 58 | .quantity .minus:hover, .quantity .plus:hover { 59 | background-color: #dad8da 60 | } 61 | 62 | .quantity .minus { 63 | bottom: 0 64 | } 65 | .shopping-cart { 66 | margin-top: 20px; 67 | } 68 | 69 | @media only screen and (max-width: 768px) { 70 | .product-name { 71 | font-size: 1rem; 72 | margin-top: .5rem; 73 | } 74 | 75 | .product-description { 76 | font-size: 1rem; 77 | } 78 | 79 | .product-quantity-container { 80 | margin-top: .5rem; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-item/shopping-cart-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingCartItemComponent } from './shopping-cart-item.component'; 4 | 5 | describe('ShoppingCartItemComponent', () => { 6 | let component: ShoppingCartItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingCartItemComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingCartItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart-item/shopping-cart-item.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | import {Product} from '../../core/models/Product'; 3 | import {Store} from '@ngrx/store'; 4 | import * as fromApp from '../../store/app.reducer'; 5 | import {DecrementCartQuantity, IncrementCartQuantity, RemoveProductFromCart} from '../../store/shop/shop.action'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-shopping-cart-item', 10 | templateUrl: './shopping-cart-item.component.html', 11 | styleUrls: ['./shopping-cart-item.component.scss'] 12 | }) 13 | export class ShoppingCartItemComponent implements OnInit { 14 | 15 | @Input('product') product: Product; 16 | 17 | constructor(private store: Store) { 18 | } 19 | 20 | ngOnInit() { 21 | } 22 | 23 | onIncrementCartItem(): void { 24 | console.log('onIncrement cart click'); 25 | this.store.dispatch(new IncrementCartQuantity(this.product.id)); 26 | } 27 | 28 | onDecrementCartItem(): void { 29 | console.log('onDecrement cart click'); 30 | this.store.dispatch(new DecrementCartQuantity(this.product.id)); 31 | } 32 | 33 | onRemoveCartItem(): void { 34 | this.store.dispatch(new RemoveProductFromCart(this.product.id)); 35 | } 36 | 37 | 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shopping-cart/shopping-cart.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ShoppingCartContainerComponent } from './shopping-cart-container/shopping-cart-container.component'; 4 | import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component'; 5 | import {CoreModule} from '../core/core.module'; 6 | 7 | @NgModule({ 8 | declarations: [ShoppingCartContainerComponent, ShoppingCartItemComponent], 9 | imports: [ 10 | CommonModule, 11 | CoreModule 12 | ], 13 | exports: [ShoppingCartContainerComponent, ShoppingCartItemComponent] 14 | }) 15 | export class ShoppingCartModule { } 16 | -------------------------------------------------------------------------------- /src/app/store/app.reducer.ts: -------------------------------------------------------------------------------- 1 | import {ActionReducerMap} from '@ngrx/store'; 2 | 3 | import * as fromShop from './shop/shop.reducer'; 4 | import * as fromBrandFilter from './brand-filter/brand-filter.reducer'; 5 | import * as fromPriceFilter from './price-filter/price-filter.reducer'; 6 | 7 | export interface AppState { 8 | shop: fromShop.State; 9 | brandFilter: string; 10 | orderBy: string; 11 | } 12 | 13 | export const reducers: ActionReducerMap = { 14 | shop: fromShop.shoppingListReducer, 15 | brandFilter: fromBrandFilter.brandFilterReducer, 16 | orderBy: fromPriceFilter.orderByPriceReducer 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /src/app/store/brand-filter/brand-filter.action.ts: -------------------------------------------------------------------------------- 1 | import {BrandFilterActionTypes} from './brand-filter.action.types'; 2 | 3 | export class AddBrandToFilter { 4 | readonly type = BrandFilterActionTypes.ADD_BRAND_TO_FILTER; 5 | 6 | constructor(public payload: string) { 7 | } 8 | } 9 | 10 | export class RemoveBrandFromFilter { 11 | readonly type = BrandFilterActionTypes.REMOVE_BRAND_FROM_FILTER; 12 | 13 | constructor(public payload: string) { 14 | } 15 | } 16 | 17 | export class ClearBrandFilter { 18 | readonly type = BrandFilterActionTypes.CLEAR_BRAND_FILTER; 19 | 20 | constructor() { 21 | } 22 | 23 | } 24 | 25 | export type BrandFilterActions = AddBrandToFilter | RemoveBrandFromFilter | ClearBrandFilter; 26 | -------------------------------------------------------------------------------- /src/app/store/brand-filter/brand-filter.action.types.ts: -------------------------------------------------------------------------------- 1 | export enum BrandFilterActionTypes { 2 | ADD_BRAND_TO_FILTER = 'ADD_BRAND_TO_FILTER', 3 | REMOVE_BRAND_FROM_FILTER = 'REMOVE_BRAND_FROM_FILTER', 4 | CLEAR_BRAND_FILTER = 'CLEAR_BRAND_FILTER' 5 | } 6 | -------------------------------------------------------------------------------- /src/app/store/brand-filter/brand-filter.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as BrandFilterActions from './brand-filter.action'; 2 | import {BrandFilterActionTypes} from './brand-filter.action.types'; 3 | 4 | 5 | export function brandFilterReducer(state: string = '', action: BrandFilterActions.BrandFilterActions) { 6 | switch (action.type) { 7 | case BrandFilterActionTypes.ADD_BRAND_TO_FILTER: 8 | if (state.includes(action.payload)) { 9 | return state; 10 | } 11 | return state += action.payload; 12 | case BrandFilterActionTypes.REMOVE_BRAND_FROM_FILTER: 13 | console.log('remove brand', action); 14 | const reg = new RegExp(action.payload, 'gi'); 15 | return state.replace(reg, ''); 16 | case BrandFilterActionTypes.CLEAR_BRAND_FILTER: 17 | return ''; 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/store/price-filter/price-filter.action.ts: -------------------------------------------------------------------------------- 1 | import {PriceFilterTypes} from './price-filter.types'; 2 | 3 | export class OrderByAsc { 4 | readonly type = PriceFilterTypes.ORDER_BY_ASC; 5 | 6 | constructor() { 7 | } 8 | } 9 | 10 | export class OrderByDesc { 11 | readonly type = PriceFilterTypes.ORDER_BY_DESC; 12 | 13 | constructor() { 14 | 15 | } 16 | } 17 | 18 | export class ClearOrderBy { 19 | readonly type = PriceFilterTypes.CLEAR_ORDER_BY_PRICE; 20 | 21 | constructor() { 22 | } 23 | } 24 | 25 | export type PriceFilterActions = OrderByAsc | OrderByDesc | ClearOrderBy; 26 | -------------------------------------------------------------------------------- /src/app/store/price-filter/price-filter.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as PriceFilterActions from './price-filter.action'; 2 | import {PriceFilterTypes} from './price-filter.types'; 3 | 4 | export const orderByPriceReducer = (state = '', action: PriceFilterActions.PriceFilterActions) => { 5 | switch (action.type) { 6 | case PriceFilterTypes.ORDER_BY_ASC: 7 | return 'asc'; 8 | case PriceFilterTypes.ORDER_BY_DESC: 9 | return 'desc'; 10 | case PriceFilterTypes.CLEAR_ORDER_BY_PRICE: 11 | return ''; 12 | default: 13 | return state; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/store/price-filter/price-filter.types.ts: -------------------------------------------------------------------------------- 1 | export enum PriceFilterTypes { 2 | ORDER_BY_ASC = 'ORDER_BY_ASC', 3 | ORDER_BY_DESC = 'ORDER_BY_DESC', 4 | CLEAR_ORDER_BY_PRICE = 'CLEAR_ORDER_BY_PRICE' 5 | } 6 | -------------------------------------------------------------------------------- /src/app/store/shop/shop.action.ts: -------------------------------------------------------------------------------- 1 | import {ShopActionTypes} from './shop.action.types'; 2 | import {Product} from '../../core/models/Product'; 3 | 4 | export class AddProductToCart { 5 | readonly type = ShopActionTypes.ADD_PRODUCT_TO_CART; 6 | 7 | constructor(public payload: Product) { 8 | } 9 | } 10 | 11 | export class RemoveProductFromCart { 12 | readonly type = ShopActionTypes.REMOVE_PRODUCT_FROM_CART; 13 | 14 | constructor(public payload: number) { 15 | } 16 | } 17 | 18 | export class IncrementCartQuantity { 19 | readonly type = ShopActionTypes.INCREMENT_CART_ITEM_QUANTITY; 20 | 21 | constructor(public payload: number) { 22 | } 23 | } 24 | 25 | export class DecrementCartQuantity { 26 | readonly type = ShopActionTypes.DECREMENT_CART_ITEM_QUANTITY; 27 | 28 | constructor(public payload: number) { 29 | } 30 | } 31 | 32 | export type ShopActions = AddProductToCart | RemoveProductFromCart | IncrementCartQuantity | DecrementCartQuantity; 33 | -------------------------------------------------------------------------------- /src/app/store/shop/shop.action.types.ts: -------------------------------------------------------------------------------- 1 | export enum ShopActionTypes { 2 | ADD_PRODUCT_TO_CART = 'ADD_PRODUCT_TO_CART', 3 | REMOVE_PRODUCT_FROM_CART = 'REMOVE_PRODUCT_FROM_CART', 4 | INCREMENT_CART_ITEM_QUANTITY = 'INCREMENT_CART_ITEM_QUANTITY', 5 | DECREMENT_CART_ITEM_QUANTITY = 'DECREMENT_CART_ITEM_QUANTITY' 6 | } 7 | -------------------------------------------------------------------------------- /src/app/store/shop/shop.reducer.ts: -------------------------------------------------------------------------------- 1 | import {Product} from '../../core/models/Product'; 2 | import {phones} from '../../core/data/phones'; 3 | 4 | import * as ShopActions from './shop.action'; 5 | import {ShopActionTypes} from './shop.action.types'; 6 | 7 | export interface State { 8 | products: Array; 9 | cart: Array; 10 | } 11 | 12 | const initialState: State = { 13 | products: phones, 14 | cart: [] 15 | }; 16 | 17 | export function shoppingListReducer(state = initialState, action: ShopActions.ShopActions) { 18 | let updatedCart; 19 | let updatedItemIndex; 20 | 21 | switch (action.type) { 22 | case ShopActionTypes.INCREMENT_CART_ITEM_QUANTITY: 23 | updatedCart = [...state.cart]; 24 | updatedItemIndex = updatedCart.findIndex( 25 | item => item.id === action.payload 26 | ); 27 | 28 | if (updatedCart[updatedItemIndex].quantity > 9) { 29 | return state; 30 | } 31 | 32 | const incrementedItem = { 33 | ...updatedCart[updatedItemIndex] 34 | }; 35 | 36 | incrementedItem.quantity++; 37 | 38 | updatedCart[updatedItemIndex] = incrementedItem; 39 | 40 | 41 | return {...state, cart: updatedCart}; 42 | 43 | case ShopActionTypes.DECREMENT_CART_ITEM_QUANTITY: 44 | updatedCart = [...state.cart]; 45 | updatedItemIndex = updatedCart.findIndex( 46 | item => item.id === action.payload 47 | ); 48 | 49 | if (updatedCart[updatedItemIndex].quantity < 2) { 50 | return state; 51 | } 52 | 53 | 54 | const decrementedItem = { 55 | ...updatedCart[updatedItemIndex] 56 | }; 57 | 58 | decrementedItem.quantity--; 59 | 60 | updatedCart[updatedItemIndex] = decrementedItem; 61 | 62 | return {...state, cart: updatedCart}; 63 | 64 | case ShopActionTypes.ADD_PRODUCT_TO_CART: 65 | updatedCart = [...state.cart]; 66 | updatedItemIndex = updatedCart.findIndex(item => item.id === action.payload.id); 67 | 68 | if (updatedItemIndex < 0) { 69 | updatedCart.push({...action.payload, quantity: 1}); 70 | } else { 71 | const updatedItem = { 72 | ...updatedCart[updatedItemIndex] 73 | }; 74 | 75 | updatedItem.quantity++; 76 | updatedCart[updatedItemIndex] = updatedItem; 77 | } 78 | 79 | return {...state, cart: updatedCart}; 80 | case ShopActionTypes.REMOVE_PRODUCT_FROM_CART: 81 | updatedCart = [...state.cart]; 82 | updatedItemIndex = updatedCart.findIndex( 83 | item => item.id === action.payload 84 | ); 85 | 86 | updatedCart.splice(updatedItemIndex, 1); 87 | 88 | return {...state, cart: updatedCart}; 89 | default: 90 | return state; 91 | 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/app/utilities/cumulativeOffset.ts: -------------------------------------------------------------------------------- 1 | import {EventTypes} from '../core/enums/event-types'; 2 | 3 | export const cumulativeOffSet = (element, type: string): { top: number, left: number } => { 4 | let top = 0; 5 | let left = 0; 6 | 7 | do { 8 | top += element.offsetTop || 0; 9 | left += element.offsetLeft || 0; 10 | element = element.offsetParent; 11 | 12 | } while (element); 13 | 14 | 15 | return { 16 | top, 17 | left 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCoderDream/Angular-Ecommerce-App-with-NGRX/f355805b76099aa488dfd02e764b72910bb01b8d/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ecommerce 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/ecommerce'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags.ts'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../node_modules/bootstrap/scss/bootstrap.scss"; 2 | 3 | 4 | html{ height:100%; } 5 | body{ min-height:100%; padding:0; margin:0; position:relative; } 6 | 7 | body::after{ content:''; display:block; height:100px; } 8 | 9 | footer{ 10 | position:absolute; 11 | bottom:0; 12 | width:100%; 13 | height:100px; 14 | } 15 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "array-type": false, 8 | "arrow-parens": false, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "import-blacklist": [ 13 | true, 14 | "rxjs/Rx" 15 | ], 16 | "interface-name": false, 17 | "max-classes-per-file": false, 18 | "max-line-length": [ 19 | true, 20 | 140 21 | ], 22 | "member-access": false, 23 | "member-ordering": [ 24 | true, 25 | { 26 | "order": [ 27 | "static-field", 28 | "instance-field", 29 | "static-method", 30 | "instance-method" 31 | ] 32 | } 33 | ], 34 | "no-consecutive-blank-lines": false, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-empty": false, 44 | "no-inferrable-types": [ 45 | true, 46 | "ignore-params" 47 | ], 48 | "no-non-null-assertion": true, 49 | "no-redundant-jsdoc": true, 50 | "no-switch-case-fall-through": true, 51 | "no-use-before-declare": true, 52 | "no-var-requires": false, 53 | "object-literal-key-quotes": [ 54 | true, 55 | "as-needed" 56 | ], 57 | "object-literal-sort-keys": false, 58 | "ordered-imports": false, 59 | "quotemark": [ 60 | true, 61 | "single" 62 | ], 63 | "trailing-comma": false, 64 | "no-output-on-prefix": true, 65 | "use-input-property-decorator": true, 66 | "use-output-property-decorator": true, 67 | "use-host-property-decorator": true, 68 | "no-input-rename": true, 69 | "no-output-rename": true, 70 | "use-life-cycle-interface": true, 71 | "use-pipe-transform-interface": true, 72 | "component-class-suffix": true, 73 | "directive-class-suffix": true 74 | } 75 | } 76 | --------------------------------------------------------------------------------