├── .angular-cli.json ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── protractor.conf.js ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app.translate.factory.ts │ ├── config │ │ └── app.config.ts │ ├── core │ │ ├── core.module.ts │ │ ├── error404 │ │ │ ├── error404.component.html │ │ │ ├── error404.component.scss │ │ │ ├── error404.component.spec.ts │ │ │ └── error404.component.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 │ │ └── progress-bar.service.ts │ ├── dashboard │ │ ├── dashboard.component.html │ │ ├── dashboard.component.scss │ │ ├── dashboard.component.spec.ts │ │ └── dashboard.component.ts │ ├── services │ │ └── state │ │ │ ├── state.service.spec.ts │ │ │ └── state.service.ts │ ├── shared │ │ ├── material.module.ts │ │ ├── shared-components.module.ts │ │ └── shared.module.ts │ └── shopping │ │ ├── shared │ │ ├── product.model.ts │ │ ├── shopping-list.model.ts │ │ ├── shopping.service.spec.ts │ │ └── shopping.service.ts │ │ ├── shopping-list-detail │ │ ├── shopping-list-detail.component.html │ │ ├── shopping-list-detail.component.scss │ │ ├── shopping-list-detail.component.spec.ts │ │ └── shopping-list-detail.component.ts │ │ ├── shopping-lists │ │ ├── remove-shopping-list.dialog.html │ │ ├── shopping-lists.component.html │ │ ├── shopping-lists.component.scss │ │ ├── shopping-lists.component.spec.ts │ │ └── shopping-lists.component.ts │ │ ├── shopping-routing.module.ts │ │ ├── shopping.component.html │ │ ├── shopping.component.spec.ts │ │ ├── shopping.component.ts │ │ └── shopping.module.ts ├── assets │ ├── .gitkeep │ ├── i18n │ │ ├── lang.de.json │ │ └── lang.en.json │ └── images │ │ ├── angular-app-screen.png │ │ ├── github-circle-white-transparent.svg │ │ └── shopping-lists │ │ └── cart7.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── scss │ ├── basic │ │ ├── _main.scss │ │ └── loading.scss │ ├── styles.scss │ └── vendors │ │ └── _normalize.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig.json └── tslint.json /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "my-app" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "scss/styles.scss" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json", 40 | "exclude": "**/node_modules/**" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json", 44 | "exclude": "**/node_modules/**" 45 | }, 46 | { 47 | "project": "e2e/tsconfig.e2e.json", 48 | "exclude": "**/node_modules/**" 49 | } 50 | ], 51 | "test": { 52 | "karma": { 53 | "config": "./karma.conf.js" 54 | } 55 | }, 56 | "defaults": { 57 | "styleExt": "scss", 58 | "component": { 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Subject of the issue 2 | Describe your issue here. 3 | 4 | ### Your environment 5 | * version of application 6 | * version of angular 7 | * which browser and its version 8 | 9 | ### Steps to reproduce 10 | Tell us how to reproduce this issue 11 | 12 | ### Expected behaviour 13 | Tell us what should happen 14 | 15 | ### Actual behaviour 16 | Tell us what happens instead 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Short description of what this resolves: 2 | 3 | 4 | #### Changes proposed in this pull request: 5 | 6 | - 7 | - 8 | - 9 | 10 | **Base Version**: 1.x / 2.x / 3.x / 4.x 11 | 12 | **Fixes**: # 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '9.10' 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | script: 10 | - npm run build 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | Please note we have a code of conduct, please follow it in all your interactions with the project. 6 | 7 | ## Our Pledge 8 | 9 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 10 | 11 | ## Our Standards 12 | 13 | Examples of behavior that contributes to creating a positive environment include: 14 | 15 | Using welcoming and inclusive language Being respectful of differing viewpoints and experiences Gracefully accepting constructive criticism Focusing on what is best for the community Showing empathy towards other community members Examples of unacceptable behavior by participants include: 16 | 17 | The use of sexualized language or imagery and unwelcome sexual attention or advances Trolling, insulting/derogatory comments, and personal or political attacks Public or private harassment Publishing others' private information, such as a physical or electronic address, without explicit permission Other conduct which could reasonably be considered inappropriate in a professional setting Our Responsibilities 18 | 19 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 20 | 21 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 22 | 23 | ## Scope 24 | 25 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 26 | 27 | ## Enforcement 28 | 29 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at antonova64@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 30 | 31 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 32 | 33 | ## Attribution 34 | 35 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anastasiia Antonova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 5 Example Shopping App + Angular Material + Responsive 2 | 3 | > ### Made with :heart: 4 | 5 | [![Join the chat at https://gitter.im/Angular-5-Example-Shopping-App/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Angular-5-Example-Shopping-App/Lobby) 6 | [![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) 7 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/) 8 | [![TypeScript](https://badges.frapsoft.com/typescript/love/typescript.svg?v=101)](https://github.com/ellerbrock/typescript-badges/) 9 | [![Build Status](https://travis-ci.org/affilnost/angular5-example-shopping-app.svg?branch=master)](https://travis-ci.org/affilnost/angular5-example-shopping-app.svg?branch=master) 10 | 11 | ## [LIVE DEMO](https://affilnost.github.io/angular5-example-shopping-app/) 12 | [![angular-example-shopping-app](https://goo.gl/kjy8Ph)](https://affilnost.github.io/angular5-example-shopping-app/) 13 | 14 | ## UI Description 15 | The app provides a possibility to maintain shopping lists. 16 | 17 | - Products can be searched with a search field 18 | - Each product can be added to a shopping list 19 | - Shopping list can be viewed 20 | - Products can be removed from shopping list 21 | - Name of shopping lists can be defined and changed 22 | - User friendly design 23 | 24 | API connection: 25 | - App uses shutterstock.com API, but it can be easily changed to any other api 26 | 27 | ## Features 28 | - Angular 5+ 29 | - Internationalization (translations) 30 | - Routing 31 | - Lazy loading (Shopping module) 32 | - Material Design 33 | - Custom loading page 34 | - Responsive layout (flex layout module) 35 | - RxJS/Observables 36 | - Angular forms 37 | - Http 38 | - Scalable architecture 39 | - Following the best practices! 40 | 41 | 42 | ## Environment installation 43 | You need to have `Node.js` and `npm` installed on your PC/Mac. 44 | 45 | Then just run `npm install` in the project's root. 46 | 47 | 48 | ## Development server 49 | 50 | Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 51 | 52 | ## Build 53 | 54 | Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. 55 | 56 | 57 | ## Possible Issues / Improvements 58 | * Pagination in the Product Search List 59 | * Possibility to add multiple products (of one type) to the Shopping List and to change the count of every product 60 | * Write tests 61 | * Navigation: Add breadcrumbs, "Back" buttons 62 | 63 | ## Contributing 64 | Please see the CONTRIBUTING file for guidelines. 65 | 66 | ## Deploying to GitHub Pages 67 | `npm run deploy-to-ghpages` 68 | 69 | ## License 70 | MIT 71 | 72 | Enjoy :stuck_out_tongue_winking_eye: 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('my-app App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /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/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build --prod", 9 | "build-gh": "ng build --prod --base-href \"/angular5-example-shopping-app/\"", 10 | "deploy-to-ghpages": "npm run build-gh && ngh", 11 | "test": "ng test", 12 | "lint": "ng lint", 13 | "e2e": "ng e2e" 14 | }, 15 | "private": true, 16 | "dependencies": { 17 | "@angular/animations": "^5.2.9", 18 | "@angular/cdk": "^5.2.4", 19 | "@angular/common": "^5.2.0", 20 | "@angular/compiler": "^5.2.0", 21 | "@angular/core": "^5.2.0", 22 | "@angular/flex-layout": "^2.0.0-beta.10-4905443", 23 | "@angular/forms": "^5.2.0", 24 | "@angular/http": "^5.2.3", 25 | "@angular/material": "^5.2.4", 26 | "@angular/platform-browser": "^5.2.0", 27 | "@angular/platform-browser-dynamic": "^5.2.0", 28 | "@angular/router": "^5.2.0", 29 | "@ngx-translate/core": "^9.0.1", 30 | "@ngx-translate/http-loader": "^2.0.1", 31 | "core-js": "^2.4.1", 32 | "rxjs": "^5.5.6", 33 | "zone.js": "^0.8.19" 34 | }, 35 | "devDependencies": { 36 | "@angular/cli": "~1.7.3", 37 | "@angular/compiler-cli": "^5.2.0", 38 | "@angular/language-service": "^5.2.0", 39 | "@types/jasmine": "~2.8.3", 40 | "@types/jasminewd2": "~2.0.2", 41 | "@types/node": "~6.0.60", 42 | "angular-cli-ghpages": "^0.5.2", 43 | "codelyzer": "^4.0.1", 44 | "jasmine-core": "~2.8.0", 45 | "jasmine-spec-reporter": "~4.2.1", 46 | "karma": "~2.0.0", 47 | "karma-chrome-launcher": "~2.2.0", 48 | "karma-coverage-istanbul-reporter": "^1.2.1", 49 | "karma-jasmine": "~1.1.0", 50 | "karma-jasmine-html-reporter": "^0.2.2", 51 | "protractor": "~5.1.2", 52 | "ts-node": "~4.1.0", 53 | "tslint": "~5.9.1", 54 | "typescript": "~2.6.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | 4 | import {DashboardComponent} from './dashboard/dashboard.component'; 5 | import {Error404Component} from './core/error404/error404.component'; 6 | import {AppConfig} from './config/app.config'; 7 | import {ShoppingListDetailComponent} from './shopping/shopping-list-detail/shopping-list-detail.component'; 8 | 9 | const routes: Routes = [ 10 | {path: '', redirectTo: '/', pathMatch: 'full'}, 11 | { 12 | path: '', component: DashboardComponent, data: {title: 'Dashboard'} 13 | }, 14 | {path: AppConfig.routes.shopping, loadChildren: 'app/shopping/shopping.module#ShoppingModule'}, 15 | {path: AppConfig.routes.error404, component: Error404Component}, 16 | {path: '**', redirectTo: '/' + AppConfig.routes.error404} 17 | ]; 18 | 19 | @NgModule({ 20 | exports: [RouterModule], 21 | imports: [RouterModule.forRoot(routes)] 22 | }) 23 | 24 | export class AppRoutingModule { 25 | } 26 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @media (max-width: 680px) { 2 | app-header { 3 | margin-bottom: 90px; 4 | } 5 | 6 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Inject} from '@angular/core'; 2 | import {TranslateService} from '@ngx-translate/core'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent { 10 | title: string; 11 | 12 | constructor(private translate: TranslateService, @Inject('state') private state) { 13 | // all languages used 14 | const langs = ['en', 'de']; 15 | const browserLang = translate.getBrowserLang(); 16 | translate.addLangs(langs); 17 | // setting the default lang - if no lang is used 18 | translate.setDefaultLang('en'); 19 | // setting the language depending on the browser setting 20 | if (langs.indexOf(browserLang) > -1) { 21 | translate.use(browserLang); 22 | } 23 | 24 | // just for testing 25 | // translate.use('de'); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | import {HttpClientModule, HttpClient} from '@angular/common/http'; 5 | 6 | // Material 7 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 8 | import {MaterialModule} from './shared/material.module'; 9 | 10 | // Translate 11 | import {TranslateModule, TranslateLoader} from '@ngx-translate/core'; 12 | import {HttpLoaderFactory} from './app.translate.factory'; 13 | 14 | // Routing 15 | import {AppRoutingModule} from './app-routing.module'; 16 | 17 | import {CoreModule} from './core/core.module'; 18 | 19 | import {AppComponent} from './app.component'; 20 | import {DashboardComponent} from './dashboard/dashboard.component'; 21 | 22 | import {SharedModule} from './shared/shared.module'; 23 | 24 | import {StateService} from './services/state/state.service'; 25 | import {APP_CONFIG, AppConfig} from './config/app.config'; 26 | 27 | import {ShoppingService} from './shopping/shared/shopping.service'; 28 | import {CommonModule} from '@angular/common'; 29 | import {SharedComponentsModule} from './shared/shared-components.module'; 30 | 31 | 32 | @NgModule({ 33 | declarations: [ 34 | AppComponent, 35 | DashboardComponent 36 | ], 37 | imports: [ 38 | CommonModule, 39 | BrowserModule, 40 | BrowserAnimationsModule, 41 | MaterialModule, 42 | FormsModule, 43 | ReactiveFormsModule, 44 | HttpClientModule, 45 | CoreModule, 46 | TranslateModule.forRoot({ 47 | loader: { 48 | provide: TranslateLoader, 49 | useFactory: HttpLoaderFactory, 50 | deps: [HttpClient] 51 | } 52 | }), 53 | SharedModule.forRoot(), 54 | AppRoutingModule, 55 | SharedComponentsModule 56 | ], 57 | providers: [ 58 | {provide: 'state', useClass: StateService}, 59 | {provide: APP_CONFIG, useValue: AppConfig}, 60 | ShoppingService 61 | ], 62 | bootstrap: [AppComponent] 63 | }) 64 | export class AppModule { 65 | } 66 | -------------------------------------------------------------------------------- /src/app/app.translate.factory.ts: -------------------------------------------------------------------------------- 1 | import {TranslateHttpLoader} from '@ngx-translate/http-loader'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | // AoT requires an exported function for factories 5 | export function HttpLoaderFactory(http: HttpClient) { 6 | return new TranslateHttpLoader(http, './assets/i18n/lang.', '.json'); 7 | } 8 | -------------------------------------------------------------------------------- /src/app/config/app.config.ts: -------------------------------------------------------------------------------- 1 | import {InjectionToken} from '@angular/core'; 2 | 3 | export let APP_CONFIG = new InjectionToken('app.config'); 4 | 5 | export const AppConfig = { 6 | routes: { 7 | shopping: 'shopping', 8 | error404: '404' 9 | }, 10 | endpoints: { 11 | productsBaseUrl: 'https://api.shutterstock.com', 12 | productsGetPath: '/v2/images/search' 13 | 14 | }, 15 | snackBarDuration: 3000, 16 | repositoryURL: 'https://github.com/affilnost/angular5-example-shopping-app', 17 | defultSHContent: '[{"id":"257450194","name":"Chicken salad with leaf vegetables and cherry toma...",' + 18 | '"imgUrl":"https://image.shutterstock.com/display_pic_with_logo/236329/257450194/stock-photo-chicken-salad' + 19 | '-with-leaf-vegetables-and-cherry-tomatoes-257450194.jpg","description":"Chicken salad with leaf vegetables' + 20 | ' and cherry tomatoes"},{"id":"548526682","name":"Easter cake on a white background","imgUrl":"https://ima' + 21 | 'ge.shutterstock.com/display_pic_with_logo/2675854/548526682/stock-photo-easter-cake-on-a-white-background' + 22 | '-548526682.jpg","description":"Easter cake on a white background"},{"id":"591617762","name":"Chocolate egg' + 23 | ' exploded","imgUrl":"https://image.shutterstock.com/display_pic_with_logo/685426/591617762/stock-photo-cho' + 24 | 'colate-egg-exploded-591617762.jpg","description":"Chocolate egg exploded"},{"id":"525754399","name":"Glass' + 25 | ' of milk isolated on white","imgUrl":"https://image.shutterstock.com/display_pic_with_logo/371512/52575439' + 26 | '9/stock-photo-glass-of-milk-isolated-on-white-525754399.jpg","description":"Glass of milk isolated on whi' + 27 | 'te"},{"id":"629596088","name":"Bread and Bakery Products Isolated on White. Diffe...","imgUrl":"https://i' + 28 | 'mage.shutterstock.com/display_pic_with_logo/3011495/629596088/stock-photo-bread-and-bakery-products-isola' + 29 | 'ted-on-white-different-types-of-bread-sesame-bun-baguette-baked-629596088.jpg","description":"Bread and Ba' + 30 | 'kery Products Isolated on White. Different types of bread: sesame bun, baguette, baked rolls, rustic brea' + 31 | 'd, round bun, sesame bun."}]' 32 | }; 33 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | 4 | import {HeaderComponent} from './header/header.component'; 5 | import {FooterComponent} from './footer/footer.component'; 6 | 7 | 8 | import {RouterModule} from '@angular/router'; 9 | import {SharedModule} from '../shared/shared.module'; 10 | 11 | import { Error404Component} from './error404/error404.component'; 12 | import {ProgressBarService} from './progress-bar.service'; 13 | 14 | @NgModule({ 15 | imports: [ 16 | RouterModule, 17 | CommonModule, 18 | SharedModule 19 | ], 20 | exports: [ 21 | HeaderComponent, 22 | FooterComponent 23 | ], 24 | declarations: [ 25 | HeaderComponent, 26 | FooterComponent, 27 | Error404Component 28 | ], 29 | providers: [ 30 | ProgressBarService 31 | ] 32 | }) 33 | export class CoreModule { 34 | } 35 | -------------------------------------------------------------------------------- /src/app/core/error404/error404.component.html: -------------------------------------------------------------------------------- 1 |

{{'error404' | translate}}

2 | -------------------------------------------------------------------------------- /src/app/core/error404/error404.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/affilnost/angular5-example-shopping-app/db184597b7e8852ab9edde57ce25993def6a3e19/src/app/core/error404/error404.component.scss -------------------------------------------------------------------------------- /src/app/core/error404/error404.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { Error404Component } from './error404.component'; 4 | 5 | describe('Error404Component', () => { 6 | let component: Error404Component; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ Error404Component ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(Error404Component); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/core/error404/error404.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error404', 5 | templateUrl: './error404.component.html', 6 | styleUrls: ['./error404.component.scss'] 7 | }) 8 | export class Error404Component { 9 | 10 | constructor() { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/app/core/footer/footer.component.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | padding: 8px 16px; 3 | color: white; 4 | background: #3f51b5; 5 | margin-top: 2em; 6 | position: sticky; 7 | top: 100%; 8 | .copyright { 9 | margin: 1em 0 1em; 10 | font-size: 0.7em; 11 | } 12 | img { 13 | width: 25%; 14 | } 15 | } 16 | 17 | .footer-xs { 18 | text-align: center; 19 | padding-top: 1em; 20 | } 21 | -------------------------------------------------------------------------------- /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} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-footer', 5 | templateUrl: './footer.component.html', 6 | styleUrls: ['./footer.component.scss'] 7 | }) 8 | export class FooterComponent { 9 | 10 | constructor() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/core/header/header.component.html: -------------------------------------------------------------------------------- 1 |
2 | 41 |
42 | 43 | 44 |
45 |
46 | -------------------------------------------------------------------------------- /src/app/core/header/header.component.scss: -------------------------------------------------------------------------------- 1 | :host(app-header) { 2 | padding-top: 0; 3 | padding-bottom: 4.3em; 4 | display: grid; 5 | 6 | header { 7 | position: fixed; 8 | z-index: 999; 9 | width: 100%; 10 | 11 | nav { 12 | display: flex; 13 | flex-wrap: wrap; 14 | align-items: center; 15 | padding: 8px 16px; 16 | color: white; 17 | height: 4em; 18 | background: #3f51b5; 19 | 20 | .mat-raised-button { 21 | color: #3f51b5; 22 | margin-right: 1em; 23 | font-weight: 400; 24 | } 25 | 26 | .progress-bar { 27 | display: flex; 28 | align-content: center; 29 | align-items: center; 30 | height: 5px; 31 | } 32 | 33 | .image-icon { 34 | width: 20px; 35 | height: 20px; 36 | } 37 | 38 | img { 39 | vertical-align: middle; 40 | } 41 | 42 | #today { 43 | font-size: 0.7em; 44 | } 45 | } 46 | } 47 | } 48 | 49 | @media (max-width: 680px) { 50 | :host(app-nav) { 51 | padding-top: 0; 52 | padding-bottom: 1em; 53 | 54 | header { 55 | position: relative; 56 | 57 | nav { 58 | height: 7em; 59 | display: flex; 60 | padding: 1em 2em 0; 61 | 62 | > div { 63 | margin-top: 1em; 64 | } 65 | 66 | #today { 67 | margin-top: 2em; 68 | } 69 | } 70 | } 71 | } 72 | 73 | :host(app-header) { 74 | header { 75 | nav { 76 | height: 10em; 77 | } 78 | } 79 | } 80 | } 81 | 82 | @media (max-width: 425px) { 83 | :host(app-nav) { 84 | padding-bottom: 0; 85 | 86 | header { 87 | nav { 88 | app-search-bar { 89 | width: 75%; 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /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, Inject} from '@angular/core'; 2 | import {ProgressBarService} from '../progress-bar.service'; 3 | import {APP_CONFIG, AppConfig} from '../../config/app.config'; 4 | import {TranslateService} from '@ngx-translate/core'; 5 | 6 | @Component({ 7 | selector: 'app-header', 8 | templateUrl: './header.component.html', 9 | styleUrls: ['./header.component.scss'] 10 | }) 11 | export class HeaderComponent { 12 | 13 | title: string; 14 | currentDate: number = Date.now(); 15 | appConfig: any; 16 | menuItems: any[]; 17 | progressBarMode: string; 18 | 19 | constructor(@Inject('state') private state, @Inject(APP_CONFIG) appConfig, 20 | private progressBarService: ProgressBarService, 21 | private translateService: TranslateService) { 22 | 23 | this.loadTitle(); 24 | this.appConfig = appConfig; 25 | this.loadMenus(); 26 | this.progressBarService.updateProgressBar$.subscribe((mode: string) => { 27 | this.progressBarMode = mode; 28 | }); 29 | } 30 | 31 | private loadTitle(): void { 32 | this.state.title.subscribe((res: string) => { 33 | this.title = res; 34 | }); 35 | } 36 | 37 | changeLanguage(language: string): void { 38 | this.translateService.use(language).subscribe(() => { 39 | this.loadMenus(); 40 | }); 41 | } 42 | 43 | private loadMenus(): void { 44 | this.translateService.get(['Pages.Dashboard.Title', 'Pages.Shopping.Title'], {}).subscribe((texts: any) => { 45 | this.menuItems = [ 46 | {link: '/', name: texts['Pages.Dashboard.Title']}, 47 | {link: '/' + AppConfig.routes.shopping, name: texts['Pages.Shopping.Title']} 48 | ]; 49 | }); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/app/core/progress-bar.service.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter, Injectable} from '@angular/core'; 2 | 3 | @Injectable() 4 | export class ProgressBarService { 5 | public updateProgressBar$: EventEmitter; 6 | 7 | private requestsRunning = 0; 8 | 9 | constructor() { 10 | this.updateProgressBar$ = new EventEmitter(); 11 | } 12 | 13 | public list(): number { 14 | return this.requestsRunning; 15 | } 16 | 17 | public increase(): void { 18 | this.requestsRunning++; 19 | if (this.requestsRunning === 1) { 20 | this.updateProgressBar$.emit('query'); 21 | } 22 | } 23 | 24 | public decrease(): void { 25 | if (this.requestsRunning > 0) { 26 | this.requestsRunning--; 27 | if (this.requestsRunning === 0) { 28 | this.updateProgressBar$.emit('none'); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |

{{ 'Pages.Dashboard.Headline' | translate}}

6 |

{{ "Pages.Dashboard.welcomeText" | translate }} {{ "Pages.Shopping.Title" | translate }}.

7 |
8 |
9 |
10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .welcome-card { 2 | margin: 2em auto; 3 | } 4 | .section-title { 5 | padding-top: 0; 6 | } 7 | p { 8 | text-align: center; 9 | } 10 | .main-dashboard-card { 11 | margin-bottom: 20px; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-dashboard', 5 | templateUrl: './dashboard.component.html', 6 | styleUrls: ['./dashboard.component.scss'] 7 | }) 8 | export class DashboardComponent { 9 | 10 | constructor() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/app/services/state/state.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { StateService } from './state.service'; 4 | 5 | describe('StateService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [StateService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([StateService], (service: StateService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/state/state.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {Observable} from 'rxjs/Observable'; 3 | import {ActivatedRouteSnapshot, NavigationEnd, Router} from '@angular/router'; 4 | import {filter, map, flatMap, merge} from 'rxjs/operators'; 5 | import {TranslateService} from '@ngx-translate/core'; 6 | import {of} from 'rxjs/observable/of'; 7 | 8 | 9 | @Injectable() 10 | export class StateService { 11 | 12 | title: Observable; 13 | 14 | constructor(private router: Router, private translate: TranslateService) { 15 | const routerEvents = router.events.pipe( 16 | filter(event => event instanceof NavigationEnd)); 17 | const langChangeEvents = translate.onLangChange; 18 | 19 | this.title = routerEvents.pipe(merge(langChangeEvents)) 20 | .pipe( 21 | map(() => { 22 | return this.getDeepestTitle(router.routerState.snapshot.root); 23 | }) 24 | ).pipe( 25 | flatMap(title => { 26 | return title ? translate.get('Pages.' + title + '.Title') : of([]) ; 27 | 28 | }) 29 | ); 30 | } 31 | 32 | private getDeepestTitle(routeSnapshot: ActivatedRouteSnapshot) { 33 | let title = routeSnapshot.data ? routeSnapshot.data['title'] : ''; 34 | if (routeSnapshot.firstChild) { 35 | title = this.getDeepestTitle(routeSnapshot.firstChild) || title; 36 | } 37 | return title; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/shared/material.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MatAutocompleteModule, 3 | MatButtonModule, 4 | MatCardModule, 5 | MatDialogModule, 6 | MatGridListModule, 7 | MatIconModule, 8 | MatInputModule, 9 | MatListModule, 10 | MatMenuModule, 11 | MatProgressBarModule, 12 | MatProgressSpinnerModule, 13 | MatSliderModule, 14 | MatSnackBarModule, 15 | MatTooltipModule 16 | } from '@angular/material'; 17 | import {NgModule} from '@angular/core'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | MatAutocompleteModule, 22 | MatButtonModule, 23 | MatCardModule, 24 | MatDialogModule, 25 | MatGridListModule, 26 | MatIconModule, 27 | MatInputModule, 28 | MatListModule, 29 | MatMenuModule, 30 | MatProgressBarModule, 31 | MatProgressSpinnerModule, 32 | MatSliderModule, 33 | MatSnackBarModule, 34 | MatTooltipModule 35 | ], 36 | exports: [ 37 | MatAutocompleteModule, 38 | MatButtonModule, 39 | MatCardModule, 40 | MatDialogModule, 41 | MatGridListModule, 42 | MatIconModule, 43 | MatInputModule, 44 | MatListModule, 45 | MatMenuModule, 46 | MatProgressBarModule, 47 | MatProgressSpinnerModule, 48 | MatSliderModule, 49 | MatSnackBarModule, 50 | MatTooltipModule 51 | ], 52 | declarations: [] 53 | }) 54 | export class MaterialModule { 55 | } 56 | -------------------------------------------------------------------------------- /src/app/shared/shared-components.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {ShoppingListDetailComponent} from '../shopping/shopping-list-detail/shopping-list-detail.component'; 3 | import {CommonModule} from '@angular/common'; 4 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 5 | import {TranslateModule} from '@ngx-translate/core'; 6 | import {MaterialModule} from './material.module'; 7 | 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | FormsModule, 13 | ReactiveFormsModule, 14 | TranslateModule, 15 | MaterialModule 16 | ], 17 | declarations: [ 18 | ShoppingListDetailComponent 19 | ], 20 | exports: [ 21 | ShoppingListDetailComponent 22 | ] 23 | }) 24 | 25 | export class SharedComponentsModule { 26 | } 27 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import {ModuleWithProviders, NgModule} from '@angular/core'; 2 | import {MaterialModule} from './material.module'; 3 | import {TranslateModule} from '@ngx-translate/core'; 4 | import {FlexLayoutModule} from '@angular/flex-layout'; 5 | import {CommonModule} from '@angular/common'; 6 | 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | MaterialModule, 12 | FlexLayoutModule, 13 | TranslateModule 14 | ], 15 | declarations: [], 16 | exports: [ 17 | MaterialModule, 18 | FlexLayoutModule, 19 | TranslateModule 20 | ] 21 | }) 22 | 23 | export class SharedModule { 24 | static forRoot(): ModuleWithProviders { 25 | return { 26 | ngModule: SharedModule, 27 | providers: [] 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shopping/shared/product.model.ts: -------------------------------------------------------------------------------- 1 | export class Product { 2 | constructor(public id: string, 3 | public name: string, 4 | public imgUrl: string, 5 | public description: string) { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/shopping/shared/shopping-list.model.ts: -------------------------------------------------------------------------------- 1 | export class ShoppingList { 2 | constructor(public id: number, 3 | public name: string) { 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/shopping/shared/shopping.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ShoppingService } from './shopping.service'; 4 | 5 | describe('ShoppingService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ShoppingService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ShoppingService], (service: ShoppingService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/shopping/shared/shopping.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient, HttpHeaders} from '@angular/common/http'; 3 | 4 | import {AppConfig} from '../../config/app.config'; 5 | 6 | import {ShoppingList} from './shopping-list.model'; 7 | import {Observable} from 'rxjs/Observable'; 8 | import {MatSnackBar, MatSnackBarConfig} from '@angular/material'; 9 | import {TranslateService} from '@ngx-translate/core'; 10 | import {catchError, map} from 'rxjs/operators'; 11 | import {Product} from './product.model'; 12 | import {_throw} from 'rxjs/observable/throw'; 13 | import {of} from 'rxjs/observable/of'; 14 | 15 | @Injectable() 16 | export class ShoppingService { 17 | 18 | private headers: HttpHeaders; 19 | private productsGetUrl: string; 20 | private productsBaseUrl: string; 21 | private translations: any; 22 | private shoppinglistPrefix: string; 23 | private shoppinglistsPrefix: string; 24 | private maxTitleLength: number; 25 | private defaultSL: string; 26 | 27 | constructor(private http: HttpClient, 28 | private translateService: TranslateService, 29 | private snackBar: MatSnackBar) { 30 | this.productsGetUrl = AppConfig.endpoints.productsBaseUrl + AppConfig.endpoints.productsGetPath; 31 | this.productsBaseUrl = AppConfig.endpoints.productsBaseUrl; 32 | this.defaultSL = AppConfig.defultSHContent; 33 | this.shoppinglistPrefix = 'shopping-list-'; 34 | this.shoppinglistsPrefix = 'shopping-lists'; 35 | this.maxTitleLength = 50; 36 | 37 | this.headers = new HttpHeaders({'Content-Type': 'application/json'}); 38 | 39 | this.translateService.get( 40 | ['shoppingListCreated', 'saved', 'shoppingListRemoved', 'productAdded', 'productRemovedFromShoppingList']) 41 | .subscribe((texts) => { 42 | this.translations = texts; 43 | }); 44 | } 45 | 46 | private handleError(error: any) { 47 | if (error instanceof Response) { 48 | return _throw(error.json()['error'] || 'backend server error'); 49 | } 50 | // in a case server returns 400 error, which means no data found 51 | return of([]); 52 | } 53 | 54 | private cropText(text: string): string { 55 | if (text.length > this.maxTitleLength) { 56 | return text.substring(0, this.maxTitleLength) + '...'; 57 | } else { 58 | return text; 59 | } 60 | } 61 | 62 | getProducts(query?: string): Observable { 63 | query = query || ''; 64 | const url = this.productsGetUrl + '?query=' + encodeURIComponent(query) 65 | + '&safe=true&image_type=photo&orientation=vertical&page=1&per_page=9'; 66 | const authorization = 'Basic MzJjZjQtODVhNjktYmI0OTEtYjA2NmItYzA0MTQtNDBmN2Q6ZGFmMTYtOWNiMGUtYzFlNmMtMTdhZWEtNmU2Y2YtNmQzM2I='; 67 | return this.http.get(url, { 68 | 'headers': { 69 | 'Authorization': authorization 70 | } 71 | }).pipe( 72 | map(response => { 73 | return response['data'].map(product => { 74 | return new Product( 75 | product['id'], 76 | this.cropText(product['description']), 77 | product['assets']['preview']['url'], 78 | product['description'] 79 | ); 80 | }); 81 | }) 82 | ).pipe(catchError( 83 | error => this.handleError(error) 84 | )); 85 | } 86 | 87 | getProductsByShoppingList(shoppingList: ShoppingList): Product[] { 88 | return JSON.parse(localStorage.getItem(this.shoppinglistPrefix + shoppingList.id)) || []; 89 | } 90 | 91 | addProductToShoppingList(product, shoppingList) { 92 | const products = this.getProductsByShoppingList(shoppingList); 93 | products.unshift(product); 94 | localStorage.setItem(this.shoppinglistPrefix + shoppingList.id, JSON.stringify(products)); 95 | return product; 96 | } 97 | 98 | removeProductFromShoppingList(product: Product, shoppingList: ShoppingList) { 99 | let slProducts = this.getProductsByShoppingList(shoppingList); 100 | slProducts = slProducts.filter(sl => sl.id !== product.id); 101 | localStorage.setItem(this.shoppinglistPrefix + shoppingList.id, JSON.stringify(slProducts)); 102 | } 103 | 104 | isThereShoppingLists(): boolean { 105 | if (!this.getAllShoppingLists().length) { 106 | return false; 107 | } 108 | return true; 109 | } 110 | 111 | getFirstShoppingList(): ShoppingList { 112 | const shLists = this.getAllShoppingLists(); 113 | if (shLists.length) { 114 | return shLists[0]; 115 | } else { 116 | return null; 117 | } 118 | } 119 | 120 | initShoppingListsWithDefault(): ShoppingList { 121 | const shoppingList = new ShoppingList(0, 'Default'); 122 | const shoppingLists = [shoppingList]; 123 | localStorage.setItem(this.shoppinglistsPrefix, JSON.stringify(shoppingLists)); 124 | localStorage.setItem(this.shoppinglistPrefix + 0, this.defaultSL); 125 | return shoppingList; 126 | } 127 | 128 | getAllShoppingLists(): ShoppingList[] { 129 | return JSON.parse(localStorage.getItem(this.shoppinglistsPrefix)) || []; 130 | } 131 | 132 | getShoppingListById(slId: string): ShoppingList { 133 | const shoppingLists = this.getAllShoppingLists().sort((a, b) => a.id - b.id); 134 | return shoppingLists.filter(sl => sl.id === parseInt(slId))[0]; 135 | } 136 | 137 | createShoppingList(shoppingList: any): ShoppingList { 138 | const shoppingLists = this.getAllShoppingLists().sort((a, b) => a.id - b.id); 139 | shoppingList.id = shoppingLists.length ? shoppingLists[shoppingLists.length - 1].id + 1 : 0; 140 | shoppingLists.push(shoppingList); 141 | localStorage.setItem(this.shoppinglistsPrefix, JSON.stringify(shoppingLists)); 142 | return shoppingList; 143 | } 144 | 145 | updateShoppingList(shoppingList: ShoppingList): ShoppingList { 146 | let shoppingLists = this.getAllShoppingLists(); 147 | shoppingLists = shoppingLists.map((sl: ShoppingList) => sl.id === shoppingList.id ? shoppingList : sl); 148 | localStorage.setItem(this.shoppinglistsPrefix, JSON.stringify(shoppingLists)); 149 | return shoppingList; 150 | } 151 | 152 | deleteShoppingListById(id: any) { 153 | let shoppingLists = this.getAllShoppingLists().sort((a, b) => a.id - b.id); 154 | localStorage.removeItem(this.shoppinglistPrefix + id); 155 | shoppingLists = shoppingLists.filter(sl => sl.id !== id); 156 | localStorage.setItem(this.shoppinglistsPrefix, JSON.stringify(shoppingLists)); 157 | } 158 | 159 | showSnackBar(name): void { 160 | const config: any = new MatSnackBarConfig(); 161 | config.duration = AppConfig.snackBarDuration; 162 | this.snackBar.open(this.translations[name], 'OK', config); 163 | } 164 | 165 | } 166 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-list-detail/shopping-list-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | {{'Pages.ShoppingList.Title' | translate}} “{{shoppingList.name}} 6 | 8 | edit 9 | 10 |

11 |
12 | 13 |
14 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | {{ slProduct.name }} 34 | {{ slProduct.price }} 35 | 36 | 37 | 38 | 42 | 43 | 44 |
45 |
46 |
47 |

{{ 'availableProducts' | translate }}

48 |
49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {{ product.name }} 60 | {{ product.price }} 61 | 62 | 63 | 64 | 68 | 69 | 70 |
71 |
72 |
73 | 74 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-list-detail/shopping-list-detail.component.scss: -------------------------------------------------------------------------------- 1 | .mat-card-title { 2 | font-size: 18px; 3 | } 4 | .product-card { 5 | margin-bottom: 15px; 6 | .mat-card-lg-image { 7 | max-width: 125px; 8 | max-height: 125px; 9 | width: auto; 10 | height: auto; 11 | margin-left: 15px; 12 | } 13 | } 14 | .edit-icon { 15 | cursor: pointer; 16 | } 17 | .invisible { 18 | display: none; 19 | } 20 | .save-btn { 21 | margin-left: 10px; 22 | } 23 | .section-title { 24 | margin-bottom: 30px; 25 | } 26 | .editname-form-container { 27 | min-height: 65px; 28 | } 29 | .mat-raised-button { 30 | margin-top:10px; 31 | } -------------------------------------------------------------------------------- /src/app/shopping/shopping-list-detail/shopping-list-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingListDetailComponent } from './shopping-list-detail.component'; 4 | 5 | describe('ShoppingListDetailComponent', () => { 6 | let component: ShoppingListDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingListDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingListDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-list-detail/shopping-list-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ShoppingList} from '../shared/shopping-list.model'; 3 | import {ShoppingService} from '../shared/shopping.service'; 4 | import {ActivatedRoute} from '@angular/router'; 5 | import {Product} from '../shared/product.model'; 6 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 7 | 8 | @Component({ 9 | selector: 'app-shopping-list-detail', 10 | templateUrl: './shopping-list-detail.component.html', 11 | styleUrls: ['./shopping-list-detail.component.scss'] 12 | }) 13 | export class ShoppingListDetailComponent { 14 | 15 | shoppingList: ShoppingList; 16 | products: Product[]; 17 | search: string; 18 | shoppingListProducts: Product[]; 19 | listNameEditMode: boolean; 20 | editShoppingListNameForm: FormGroup; 21 | 22 | constructor(private shoppingService: ShoppingService, private activatedRoute: ActivatedRoute, private formBuilder: FormBuilder) { 23 | this.editShoppingListNameForm = this.formBuilder.group({ 24 | 'shoppingListName': ['', [Validators.required]] 25 | }); 26 | this.activatedRoute.params.subscribe((params: any) => { 27 | if (params['id']) { 28 | this.shoppingList = this.shoppingService.getShoppingListById(params['id']); 29 | } else { 30 | if (this.shoppingService.isThereShoppingLists()) { 31 | // if there are any shopping lists - get the first one 32 | this.shoppingList = this.shoppingService.getFirstShoppingList(); 33 | 34 | } else { 35 | // if there are no shopping lists 36 | this.shoppingList = this.shoppingService.initShoppingListsWithDefault(); 37 | } 38 | } 39 | this.shoppingListProducts = this.shoppingService.getProductsByShoppingList(this.shoppingList); 40 | }); 41 | 42 | this.shoppingService.getProducts().subscribe((products: Array) => { 43 | this.products = products; 44 | }); 45 | 46 | this.listNameEditMode = false; 47 | } 48 | 49 | saveShoppingListName(formData: object) { 50 | const slName = formData['shoppingListName']; 51 | this.shoppingList.name = slName; 52 | this.shoppingService.updateShoppingList(this.shoppingList); 53 | this.shoppingService.showSnackBar('saved'); 54 | 55 | } 56 | 57 | editShoppingListName() { 58 | if (this.listNameEditMode) { 59 | this.listNameEditMode = false; 60 | } else { 61 | this.listNameEditMode = true; 62 | } 63 | } 64 | 65 | removeProductFromShoppingList(product: Product) { 66 | this.shoppingListProducts = this.shoppingListProducts.filter(pr => pr.id !== product.id); 67 | this.shoppingService.removeProductFromShoppingList(product, this.shoppingList); 68 | this.shoppingService.showSnackBar('productRemovedFromShoppingList'); 69 | 70 | } 71 | 72 | getProducts() { 73 | this.shoppingService.getProducts(this.search).subscribe((products: Array) => { 74 | this.products = products; 75 | }); 76 | } 77 | 78 | addProductToCurrentList(product) { 79 | this.shoppingService.addProductToShoppingList(product, this.shoppingList); 80 | this.shoppingService.showSnackBar('productAdded'); 81 | this.shoppingListProducts.unshift(product); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-lists/remove-shopping-list.dialog.html: -------------------------------------------------------------------------------- 1 |

{{'removeShoppingList' | translate}}

2 | {{'areYouSure' | translate}} 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-lists/shopping-lists.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ 'shoppingLists' | translate }}

3 |
4 | 8 | 9 | 10 | 11 | 14 |

{{shoppingList.name}}

15 |
16 | 18 | delete 19 | 20 | 22 | edit 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |

{{ 'createShoppingList' | translate }}

31 |
32 |
33 | 34 | 37 | 38 | 39 | 42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-lists/shopping-lists.component.scss: -------------------------------------------------------------------------------- 1 | .left-col { 2 | width: 50%; 3 | float: left; 4 | margin-left: 6%; 5 | min-height: 800px; 6 | } 7 | 8 | .right-col { 9 | margin-left: 50%; 10 | text-align: center; 11 | padding-right: 17%; 12 | } 13 | 14 | .section-title { 15 | margin-bottom: 60px; 16 | } 17 | 18 | .clear { 19 | clear: both; 20 | } 21 | 22 | .mat-list { 23 | margin: 0 auto; 24 | display: table; 25 | 26 | /deep/ .mat-list-text { 27 | text-align: left !important; 28 | } 29 | .shopping-list-item { 30 | margin-bottom: 1.5em; 31 | } 32 | } 33 | 34 | .sl-actions { 35 | padding-bottom: 0; 36 | 37 | .remove-icon, .edit-icon { 38 | padding-left: 0.5em; 39 | color: darkslategrey; 40 | cursor: pointer; 41 | } 42 | .edit-icon { 43 | padding-left: 0.3em; 44 | } 45 | } 46 | 47 | form { 48 | display: grid; 49 | width: 80%; 50 | margin: 0 auto; 51 | 52 | .mat-raised-button { 53 | color: white; 54 | background: #3f51b5; 55 | width: 50%; 56 | margin: 0 auto; 57 | } 58 | 59 | .mat-form-field { 60 | width: 100%; 61 | } 62 | } 63 | 64 | @media (max-width: 680px) { 65 | .left-col { 66 | width: 100%; 67 | float: none; 68 | margin: 0 auto; 69 | } 70 | 71 | .right-col { 72 | margin: 1em auto 0; 73 | width: 90%; 74 | float: none; 75 | padding: 0; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-lists/shopping-lists.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingListsComponent } from './shopping-lists.component'; 4 | 5 | describe('ShoppingListsComponent', () => { 6 | let component: ShoppingListsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingListsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingListsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-lists/shopping-lists.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewChild} from '@angular/core'; 2 | import {ShoppingList} from '../shared/shopping-list.model'; 3 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 4 | import {ShoppingService} from '../shared/shopping.service'; 5 | import {MatDialog} from '@angular/material'; 6 | import {Router} from '@angular/router'; 7 | import {AppConfig} from '../../config/app.config'; 8 | 9 | @Component({ 10 | selector: 'app-remove-shopping-list-dialog', 11 | templateUrl: './remove-shopping-list.dialog.html', 12 | }) 13 | 14 | export class RemoveShoppingListDialogComponent { 15 | constructor() { 16 | } 17 | } 18 | 19 | 20 | @Component({ 21 | selector: 'app-shopping-lists', 22 | templateUrl: './shopping-lists.component.html', 23 | styleUrls: ['./shopping-lists.component.scss'] 24 | }) 25 | export class ShoppingListsComponent { 26 | 27 | shoppingLists: ShoppingList[]; 28 | newShoppingListForm: FormGroup; 29 | 30 | @ViewChild('form') myNgForm; // just to call resetForm method 31 | 32 | constructor(private shoppingService: ShoppingService, 33 | private dialog: MatDialog, 34 | private router: Router, 35 | private formBuilder: FormBuilder) { 36 | this.newShoppingListForm = this.formBuilder.group({ 37 | 'name': ['', [Validators.required]] 38 | }); 39 | this.shoppingLists = this.shoppingService.getAllShoppingLists().sort( 40 | (a, b) => { 41 | return b.id - a.id; 42 | } 43 | ); 44 | } 45 | 46 | createNewShoppingList(newShoppingList: ShoppingList) { 47 | const newSLWithId = this.shoppingService.createShoppingList(newShoppingList); 48 | this.shoppingLists.unshift(newSLWithId); 49 | this.myNgForm.resetForm(); 50 | this.shoppingService.showSnackBar('shoppingListCreated'); 51 | } 52 | 53 | seeShoppingListDetails(shoppingList): void { 54 | this.router.navigate([AppConfig.routes.shopping + '/' + shoppingList.id]); 55 | } 56 | 57 | remove(shoppingListToRemove: ShoppingList): void { 58 | const dialogRef = this.dialog.open(RemoveShoppingListDialogComponent); 59 | dialogRef.afterClosed().subscribe(result => { 60 | if (result) { 61 | this.shoppingService.deleteShoppingListById(shoppingListToRemove.id); 62 | this.shoppingLists = this.shoppingLists.filter(sl => sl.id !== shoppingListToRemove.id); 63 | this.shoppingService.showSnackBar('shoppingListRemoved'); 64 | } 65 | }); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/app/shopping/shopping-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | 4 | import {ShoppingComponent} from './shopping.component'; 5 | import {ShoppingListsComponent} from './shopping-lists/shopping-lists.component'; 6 | import {ShoppingListDetailComponent} from './shopping-list-detail/shopping-list-detail.component'; 7 | 8 | const shoppingRoutes: Routes = [ 9 | { 10 | path: '', 11 | component: ShoppingComponent, 12 | children: [ 13 | {path: '', component: ShoppingListsComponent, data: {'title': 'Shopping'} }, 14 | {path: ':id', component: ShoppingListDetailComponent, data: {'title': 'ShoppingList'}} 15 | ] 16 | } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [ 21 | RouterModule.forChild(shoppingRoutes) 22 | ], 23 | exports: [ 24 | RouterModule 25 | ] 26 | }) 27 | 28 | export class ShoppingRoutingModule { 29 | } 30 | -------------------------------------------------------------------------------- /src/app/shopping/shopping.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/shopping/shopping.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ShoppingComponent } from './shopping.component'; 4 | 5 | describe('ShoppingComponent', () => { 6 | let component: ShoppingComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ShoppingComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ShoppingComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shopping/shopping.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-shopping', 5 | templateUrl: './shopping.component.html' 6 | }) 7 | export class ShoppingComponent { 8 | 9 | constructor() { } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shopping/shopping.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {CommonModule} from '@angular/common'; 3 | import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 4 | 5 | import {ShoppingRoutingModule} from './shopping-routing.module'; 6 | import {SharedModule} from '../shared/shared.module'; 7 | 8 | 9 | import {ShoppingListsComponent, RemoveShoppingListDialogComponent} from './shopping-lists/shopping-lists.component'; 10 | import {ShoppingService} from './shared/shopping.service'; 11 | import {ShoppingComponent} from './shopping.component'; 12 | import {SharedComponentsModule} from '../shared/shared-components.module'; 13 | 14 | 15 | @NgModule({ 16 | imports: [ 17 | CommonModule, 18 | FormsModule, 19 | SharedModule, 20 | ShoppingRoutingModule, 21 | ReactiveFormsModule, 22 | SharedComponentsModule 23 | ], 24 | declarations: [ 25 | ShoppingComponent, 26 | ShoppingListsComponent, 27 | RemoveShoppingListDialogComponent 28 | ], 29 | entryComponents: [ 30 | RemoveShoppingListDialogComponent 31 | ], 32 | providers: [ 33 | ShoppingService 34 | ] 35 | }) 36 | export class ShoppingModule { 37 | } 38 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/affilnost/angular5-example-shopping-app/db184597b7e8852ab9edde57ce25993def6a3e19/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/i18n/lang.de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Pages": { 3 | "Dashboard": { 4 | "Title": "Dashboard", 5 | "Headline": "Angular 5 Beispiel Shopping App", 6 | "welcomeText": "Hier können Sie Ihre Einkaufslisten erstellen und bearbeiten:" 7 | }, 8 | "Shopping": { 9 | "Title": "Einkaufslisten", 10 | "Headline": "Willkommen in Einkaufen" 11 | }, 12 | "ShoppingList": { 13 | "Title": "Einkaufsliste" 14 | } 15 | }, 16 | "error404": "404 Error", 17 | "angularShoppingListsApp": "Angular 5 Beispiel Shopping App", 18 | "shoppingLists": "Einkaufslisten", 19 | "shoppingListCreated": "Einkaufsliste ist erstellt", 20 | "saved": "Gespeichert", 21 | "shoppingListRemoved": "Einkaufsliste ist gelöscht", 22 | "shoppingListDefault": "Es ist eine Standard-Einkaufsliste, Sie können sie nicht löschen", 23 | "createShoppingList": "Einkaufsliste erstellen", 24 | "removeShoppingList": "Einkaufsliste löschen", 25 | "areYouSure": "Sind Sie sicher?", 26 | "name": "Name", 27 | "create": "Erstellen", 28 | "yes": "Ja", 29 | "no": "Nein", 30 | "availableProducts": "Verfügbare Produkte", 31 | "addToShoppingList": "Zur Einkaufsliste hinzufügen", 32 | "productSearch": "Produktsuche", 33 | "productAdded": "Das Produkt wurde hinzugefügt", 34 | "productRemovedFromShoppingList": "Das Produkt wurde entfernt", 35 | "removeFromShoppingList": "Aus der Einkaufsliste entfernen", 36 | "shoppingListName": "Shopping list name", 37 | "save": "Speichern" 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/i18n/lang.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Pages": { 3 | "Dashboard": { 4 | "Title": "Dashboard", 5 | "Headline": "Angular 5 Example Shopping App", 6 | "welcomeText": "Here you can create and edit your shopping lists:" 7 | }, 8 | "Shopping": { 9 | "Title": "Shopping Lists", 10 | "Headline": "Welcome to Shopping" 11 | }, 12 | "ShoppingList": { 13 | "Title": "Shopping List" 14 | } 15 | }, 16 | "error404": "404 Error", 17 | "angularShoppingListsApp": "Angular 5 Example Shopping App", 18 | "shoppingLists": "Shopping lists", 19 | "shoppingListCreated": "Shopping list is created", 20 | "saved": "Saved", 21 | "shoppingListRemoved": "Shopping list is removed", 22 | "shoppingListDefault": "It's a default shopping list, you cannot delete it", 23 | "createShoppingList": "Create shopping list", 24 | "removeShoppingList": "Remove shopping list", 25 | "areYouSure": "Are you sure?", 26 | "name": "Name", 27 | "create": "Create", 28 | "yes": "Yes", 29 | "no": "No", 30 | "availableProducts": "Available products", 31 | "addToShoppingList": "Add to shopping list", 32 | "productSearch": "Search products", 33 | "productAdded": "The product is added", 34 | "productRemovedFromShoppingList": "The product is removed", 35 | "removeFromShoppingList": "Remove from shopping list", 36 | "shoppingListName": "Shopping list name", 37 | "save": "Save" 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/images/angular-app-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/affilnost/angular5-example-shopping-app/db184597b7e8852ab9edde57ce25993def6a3e19/src/assets/images/angular-app-screen.png -------------------------------------------------------------------------------- /src/assets/images/github-circle-white-transparent.svg: -------------------------------------------------------------------------------- 1 | 2 | github-circle-white-transparent 3 | 5 | -------------------------------------------------------------------------------- /src/assets/images/shopping-lists/cart7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/affilnost/angular5-example-shopping-app/db184597b7e8852ab9edde57ce25993def6a3e19/src/assets/images/shopping-lists/cart7.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/affilnost/angular5-example-shopping-app/db184597b7e8852ab9edde57ce25993def6a3e19/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 5 Example Shopping App + Angular Material + Responsive 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /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.log(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/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | /** 56 | * By default, zone.js will patch all possible macroTask and DomEvents 57 | * user can disable parts of macroTask/DomEvents patch by setting following flags 58 | */ 59 | 60 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 61 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 62 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 63 | 64 | /* 65 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 66 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 67 | */ 68 | // (window as any).__Zone_enable_cross_context_check = true; 69 | 70 | /*************************************************************************************************** 71 | * Zone JS is required by default for Angular itself. 72 | */ 73 | import 'zone.js/dist/zone'; // Included with Angular CLI. 74 | 75 | 76 | 77 | /*************************************************************************************************** 78 | * APPLICATION IMPORTS 79 | */ 80 | -------------------------------------------------------------------------------- /src/scss/basic/_main.scss: -------------------------------------------------------------------------------- 1 | .twoColFlex { 2 | display: flex; 3 | align-items: flex-start; 4 | justify-content: center; 5 | flex-direction: row; 6 | flex-wrap: wrap; 7 | align-content: stretch; 8 | .col { 9 | width: 400px; 10 | min-width: 200px; 11 | margin: 0 20px 20px 20px; 12 | } 13 | } 14 | .productImg { 15 | width: 80px; 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/basic/loading.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | min-height: 100%; 7 | } 8 | 9 | .loading-page { 10 | height: 100%; 11 | width: 100%; 12 | position: absolute; 13 | background: #3f51b5; 14 | } 15 | 16 | .spinner { 17 | position: absolute; 18 | left: 48%; 19 | top: 40%; 20 | width: 55px; 21 | height: 65px; 22 | text-align: center; 23 | } 24 | 25 | .spinner { 26 | position: absolute; 27 | left: 48%; 28 | top: 40%; 29 | text-align: center; 30 | margin: 20px auto; 31 | width: 40px; 32 | height: 40px; 33 | -webkit-transform: rotateZ(45deg); 34 | transform: rotateZ(45deg); 35 | } 36 | 37 | .spinner .sk-cube { 38 | float: left; 39 | width: 50%; 40 | height: 50%; 41 | position: relative; 42 | -webkit-transform: scale(1.1); 43 | -ms-transform: scale(1.1); 44 | transform: scale(1.1); 45 | } 46 | 47 | .spinner .sk-cube:before { 48 | content: ''; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | width: 100%; 53 | height: 100%; 54 | background-color: white; 55 | -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both; 56 | animation: sk-foldCubeAngle 2.4s infinite linear both; 57 | -webkit-transform-origin: 100% 100%; 58 | -ms-transform-origin: 100% 100%; 59 | transform-origin: 100% 100%; 60 | } 61 | 62 | .spinner .sk-cube2 { 63 | -webkit-transform: scale(1.1) rotateZ(90deg); 64 | transform: scale(1.1) rotateZ(90deg); 65 | } 66 | 67 | .spinner .sk-cube3 { 68 | -webkit-transform: scale(1.1) rotateZ(180deg); 69 | transform: scale(1.1) rotateZ(180deg); 70 | } 71 | 72 | .spinner .sk-cube4 { 73 | -webkit-transform: scale(1.1) rotateZ(270deg); 74 | transform: scale(1.1) rotateZ(270deg); 75 | } 76 | 77 | .spinner .sk-cube2:before { 78 | -webkit-animation-delay: 0.3s; 79 | animation-delay: 0.3s; 80 | } 81 | 82 | .spinner .sk-cube3:before { 83 | -webkit-animation-delay: 0.6s; 84 | animation-delay: 0.6s; 85 | } 86 | 87 | .spinner .sk-cube4:before { 88 | -webkit-animation-delay: 0.9s; 89 | animation-delay: 0.9s; 90 | } 91 | 92 | @-webkit-keyframes sk-foldCubeAngle { 93 | 0%, 10% { 94 | -webkit-transform: perspective(140px) rotateX(-180deg); 95 | transform: perspective(140px) rotateX(-180deg); 96 | opacity: 0; 97 | } 98 | 25%, 75% { 99 | -webkit-transform: perspective(140px) rotateX(0deg); 100 | transform: perspective(140px) rotateX(0deg); 101 | opacity: 1; 102 | } 103 | 90%, 100% { 104 | -webkit-transform: perspective(140px) rotateY(180deg); 105 | transform: perspective(140px) rotateY(180deg); 106 | opacity: 0; 107 | } 108 | } 109 | 110 | @keyframes sk-foldCubeAngle { 111 | 0%, 10% { 112 | -webkit-transform: perspective(140px) rotateX(-180deg); 113 | transform: perspective(140px) rotateX(-180deg); 114 | opacity: 0; 115 | } 116 | 25%, 75% { 117 | -webkit-transform: perspective(140px) rotateX(0deg); 118 | transform: perspective(140px) rotateX(0deg); 119 | opacity: 1; 120 | } 121 | 90%, 100% { 122 | -webkit-transform: perspective(140px) rotateY(180deg); 123 | transform: perspective(140px) rotateY(180deg); 124 | opacity: 0; 125 | } 126 | } -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | // IMPORTS 2 | 3 | // - VENDORS 4 | @import './vendors/normalize'; 5 | 6 | // - BASIC 7 | @import './basic/main'; 8 | 9 | // - LOADING 10 | @import './basic/loading'; 11 | 12 | 13 | // - Angular material theme 14 | @import '~@angular/material/prebuilt-themes/deeppurple-amber.css'; 15 | 16 | * { 17 | font-family: 'Roboto', sans-serif; 18 | } 19 | 20 | a, .cp { 21 | cursor: pointer; 22 | } 23 | 24 | .section-title { 25 | text-align: center; 26 | font-size: 1.5em; 27 | font-weight: 300; 28 | padding: 1em 0 0; 29 | } 30 | 31 | .flex-spacer { 32 | flex-grow: 1; 33 | } 34 | 35 | .progress-spinner { 36 | height: 60px !important; 37 | margin: 2em auto; 38 | } 39 | 40 | snack-bar-container { 41 | background: #3f51b5 !important; 42 | } 43 | 44 | .mat-menu-content { 45 | background: #3f51b5; 46 | } 47 | 48 | .mat-menu-item, .mat-menu-item .mat-icon { 49 | color: white; 50 | font-weight: 100; 51 | } 52 | 53 | .sl-actions { 54 | align-items: center; 55 | display: flex; 56 | padding-bottom: 1em; 57 | } 58 | 59 | .like-icon { 60 | color: #DC143C; 61 | cursor: pointer; 62 | } 63 | 64 | .mat-raised-button[disabled] { 65 | background: #bbb !important; 66 | } 67 | 68 | .mat-dialog-container { 69 | background: #3f51b5; 70 | color: white; 71 | } 72 | 73 | .mat-dialog-content { 74 | text-align: center; 75 | padding: 1em 24px !important; 76 | } 77 | 78 | .mat-dialog-title { 79 | text-align: center; 80 | } 81 | 82 | .mat-card-image { 83 | margin-bottom: -28px !important; 84 | border-top: 1px solid #eee; 85 | } 86 | 87 | .mat-list-avatar { 88 | height: 55px !important; 89 | width: 55px !important; 90 | } 91 | 92 | .text-center { 93 | text-align: center; 94 | } 95 | 96 | .text-right { 97 | text-align: right; 98 | } 99 | 100 | pre { 101 | background: #f4f4f4; 102 | border: 1px solid #ddd; 103 | border-left: 3px solid #3f51b5; 104 | color: #666; 105 | page-break-inside: avoid; 106 | font-family: monospace; 107 | font-size: 15px; 108 | line-height: 1.6; 109 | margin-bottom: 1.6em; 110 | max-width: 100%; 111 | overflow: auto; 112 | padding: 1em 1.5em; 113 | display: block; 114 | text-align: left; 115 | word-wrap: break-word; 116 | } 117 | 118 | 119 | -------------------------------------------------------------------------------- /src/scss/vendors/_normalize.scss: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | 12 | html { 13 | line-height: 1.15; /* 1 */ 14 | -ms-text-size-adjust: 100%; /* 2 */ 15 | -webkit-text-size-adjust: 100%; /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers (opinionated). 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Add the correct display in IE 9-. 31 | */ 32 | 33 | article, 34 | aside, 35 | footer, 36 | header, 37 | nav, 38 | section { 39 | display: block; 40 | } 41 | 42 | /** 43 | * Correct the font size and margin on `h1` elements within `section` and 44 | * `article` contexts in Chrome, Firefox, and Safari. 45 | */ 46 | 47 | h1 { 48 | font-size: 2em; 49 | margin: 0.67em 0; 50 | } 51 | 52 | /* Grouping content 53 | ========================================================================== */ 54 | 55 | /** 56 | * Add the correct display in IE 9-. 57 | * 1. Add the correct display in IE. 58 | */ 59 | 60 | figcaption, 61 | figure, 62 | main { /* 1 */ 63 | display: block; 64 | } 65 | 66 | /** 67 | * Add the correct margin in IE 8. 68 | */ 69 | 70 | figure { 71 | margin: 1em 40px; 72 | } 73 | 74 | /** 75 | * 1. Add the correct box sizing in Firefox. 76 | * 2. Show the overflow in Edge and IE. 77 | */ 78 | 79 | hr { 80 | box-sizing: content-box; /* 1 */ 81 | height: 0; /* 1 */ 82 | overflow: visible; /* 2 */ 83 | } 84 | 85 | /** 86 | * 1. Correct the inheritance and scaling of font size in all browsers. 87 | * 2. Correct the odd `em` font sizing in all browsers. 88 | */ 89 | 90 | pre { 91 | font-family: monospace, monospace; /* 1 */ 92 | font-size: 1em; /* 2 */ 93 | } 94 | 95 | /* Text-level semantics 96 | ========================================================================== */ 97 | 98 | /** 99 | * 1. Remove the gray background on active links in IE 10. 100 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 101 | */ 102 | 103 | a { 104 | background-color: transparent; /* 1 */ 105 | -webkit-text-decoration-skip: objects; /* 2 */ 106 | } 107 | 108 | /** 109 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 110 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 111 | */ 112 | 113 | abbr[title] { 114 | border-bottom: none; /* 1 */ 115 | text-decoration: underline; /* 2 */ 116 | text-decoration: underline dotted; /* 2 */ 117 | } 118 | 119 | /** 120 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: inherit; 126 | } 127 | 128 | /** 129 | * Add the correct font weight in Chrome, Edge, and Safari. 130 | */ 131 | 132 | b, 133 | strong { 134 | font-weight: bolder; 135 | } 136 | 137 | /** 138 | * 1. Correct the inheritance and scaling of font size in all browsers. 139 | * 2. Correct the odd `em` font sizing in all browsers. 140 | */ 141 | 142 | code, 143 | kbd, 144 | samp { 145 | font-family: monospace, monospace; /* 1 */ 146 | font-size: 1em; /* 2 */ 147 | } 148 | 149 | /** 150 | * Add the correct font style in Android 4.3-. 151 | */ 152 | 153 | dfn { 154 | font-style: italic; 155 | } 156 | 157 | /** 158 | * Add the correct background and color in IE 9-. 159 | */ 160 | 161 | mark { 162 | background-color: #ff0; 163 | color: #000; 164 | } 165 | 166 | /** 167 | * Add the correct font size in all browsers. 168 | */ 169 | 170 | small { 171 | font-size: 80%; 172 | } 173 | 174 | /** 175 | * Prevent `sub` and `sup` elements from affecting the line height in 176 | * all browsers. 177 | */ 178 | 179 | sub, 180 | sup { 181 | font-size: 75%; 182 | line-height: 0; 183 | position: relative; 184 | vertical-align: baseline; 185 | } 186 | 187 | sub { 188 | bottom: -0.25em; 189 | } 190 | 191 | sup { 192 | top: -0.5em; 193 | } 194 | 195 | /* Embedded content 196 | ========================================================================== */ 197 | 198 | /** 199 | * Add the correct display in IE 9-. 200 | */ 201 | 202 | audio, 203 | video { 204 | display: inline-block; 205 | } 206 | 207 | /** 208 | * Add the correct display in iOS 4-7. 209 | */ 210 | 211 | audio:not([controls]) { 212 | display: none; 213 | height: 0; 214 | } 215 | 216 | /** 217 | * Remove the border on images inside links in IE 10-. 218 | */ 219 | 220 | img { 221 | border-style: none; 222 | } 223 | 224 | /** 225 | * Hide the overflow in IE. 226 | */ 227 | 228 | svg:not(:root) { 229 | overflow: hidden; 230 | } 231 | 232 | /* Forms 233 | ========================================================================== */ 234 | 235 | /** 236 | * 1. Change the font styles in all browsers (opinionated). 237 | * 2. Remove the margin in Firefox and Safari. 238 | */ 239 | 240 | button, 241 | input, 242 | optgroup, 243 | select, 244 | textarea { 245 | font-family: sans-serif; /* 1 */ 246 | font-size: 100%; /* 1 */ 247 | line-height: 1.15; /* 1 */ 248 | margin: 0; /* 2 */ 249 | } 250 | 251 | /** 252 | * Show the overflow in IE. 253 | * 1. Show the overflow in Edge. 254 | */ 255 | 256 | button, 257 | input { /* 1 */ 258 | overflow: visible; 259 | } 260 | 261 | /** 262 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 263 | * 1. Remove the inheritance of text transform in Firefox. 264 | */ 265 | 266 | button, 267 | select { /* 1 */ 268 | text-transform: none; 269 | } 270 | 271 | /** 272 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 273 | * controls in Android 4. 274 | * 2. Correct the inability to style clickable types in iOS and Safari. 275 | */ 276 | 277 | button, 278 | html [type="button"], /* 1 */ 279 | [type="reset"], 280 | [type="submit"] { 281 | -webkit-appearance: button; /* 2 */ 282 | } 283 | 284 | /** 285 | * Remove the inner border and padding in Firefox. 286 | */ 287 | 288 | button::-moz-focus-inner, 289 | [type="button"]::-moz-focus-inner, 290 | [type="reset"]::-moz-focus-inner, 291 | [type="submit"]::-moz-focus-inner { 292 | border-style: none; 293 | padding: 0; 294 | } 295 | 296 | /** 297 | * Restore the focus styles unset by the previous rule. 298 | */ 299 | 300 | button:-moz-focusring, 301 | [type="button"]:-moz-focusring, 302 | [type="reset"]:-moz-focusring, 303 | [type="submit"]:-moz-focusring { 304 | outline: 1px dotted ButtonText; 305 | } 306 | 307 | /** 308 | * Correct the padding in Firefox. 309 | */ 310 | 311 | fieldset { 312 | padding: 0.35em 0.75em 0.625em; 313 | } 314 | 315 | /** 316 | * 1. Correct the text wrapping in Edge and IE. 317 | * 2. Correct the color inheritance from `fieldset` elements in IE. 318 | * 3. Remove the padding so developers are not caught out when they zero out 319 | * `fieldset` elements in all browsers. 320 | */ 321 | 322 | legend { 323 | box-sizing: border-box; /* 1 */ 324 | color: inherit; /* 2 */ 325 | display: table; /* 1 */ 326 | max-width: 100%; /* 1 */ 327 | padding: 0; /* 3 */ 328 | white-space: normal; /* 1 */ 329 | } 330 | 331 | /** 332 | * 1. Add the correct display in IE 9-. 333 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 334 | */ 335 | 336 | progress { 337 | display: inline-block; /* 1 */ 338 | vertical-align: baseline; /* 2 */ 339 | } 340 | 341 | /** 342 | * Remove the default vertical scrollbar in IE. 343 | */ 344 | 345 | textarea { 346 | overflow: auto; 347 | } 348 | 349 | /** 350 | * 1. Add the correct box sizing in IE 10-. 351 | * 2. Remove the padding in IE 10-. 352 | */ 353 | 354 | [type="checkbox"], 355 | [type="radio"] { 356 | box-sizing: border-box; /* 1 */ 357 | padding: 0; /* 2 */ 358 | } 359 | 360 | /** 361 | * Correct the cursor style of increment and decrement buttons in Chrome. 362 | */ 363 | 364 | [type="number"]::-webkit-inner-spin-button, 365 | [type="number"]::-webkit-outer-spin-button { 366 | height: auto; 367 | } 368 | 369 | /** 370 | * 1. Correct the odd appearance in Chrome and Safari. 371 | * 2. Correct the outline style in Safari. 372 | */ 373 | 374 | [type="search"] { 375 | -webkit-appearance: textfield; /* 1 */ 376 | outline-offset: -2px; /* 2 */ 377 | } 378 | 379 | /** 380 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 381 | */ 382 | 383 | [type="search"]::-webkit-search-cancel-button, 384 | [type="search"]::-webkit-search-decoration { 385 | -webkit-appearance: none; 386 | } 387 | 388 | /** 389 | * 1. Correct the inability to style clickable types in iOS and Safari. 390 | * 2. Change font properties to `inherit` in Safari. 391 | */ 392 | 393 | ::-webkit-file-upload-button { 394 | -webkit-appearance: button; /* 1 */ 395 | font: inherit; /* 2 */ 396 | } 397 | 398 | /* Interactive 399 | ========================================================================== */ 400 | 401 | /* 402 | * Add the correct display in IE 9-. 403 | * 1. Add the correct display in Edge, IE, and Firefox. 404 | */ 405 | 406 | details, /* 1 */ 407 | menu { 408 | display: block; 409 | } 410 | 411 | /* 412 | * Add the correct display in all browsers. 413 | */ 414 | 415 | summary { 416 | display: list-item; 417 | } 418 | 419 | /* Scripting 420 | ========================================================================== */ 421 | 422 | /** 423 | * Add the correct display in IE 9-. 424 | */ 425 | 426 | canvas { 427 | display: inline-block; 428 | } 429 | 430 | /** 431 | * Add the correct display in IE. 432 | */ 433 | 434 | template { 435 | display: none; 436 | } 437 | 438 | /* Hidden 439 | ========================================================================== */ 440 | 441 | /** 442 | * Add the correct display in IE 10-. 443 | */ 444 | 445 | [hidden] { 446 | display: none; 447 | } -------------------------------------------------------------------------------- /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 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "directive-selector": [ 121 | true, 122 | "attribute", 123 | "app", 124 | "camelCase" 125 | ], 126 | "component-selector": [ 127 | true, 128 | "element", 129 | "app", 130 | "kebab-case" 131 | ], 132 | "no-output-on-prefix": true, 133 | "use-input-property-decorator": true, 134 | "use-output-property-decorator": true, 135 | "use-host-property-decorator": true, 136 | "no-input-rename": true, 137 | "no-output-rename": true, 138 | "use-life-cycle-interface": true, 139 | "use-pipe-transform-interface": true, 140 | "component-class-suffix": true, 141 | "directive-class-suffix": true 142 | } 143 | } 144 | --------------------------------------------------------------------------------