├── src ├── assets │ ├── .gitkeep │ ├── images │ │ ├── Stars.png │ │ ├── Martian.png │ │ ├── logout.png │ │ ├── users │ │ │ └── user.jpg │ │ ├── logo_full_dark.png │ │ ├── netmedia-logo.png │ │ └── netmedia-logo-grey.png │ └── fonts │ │ ├── data-table.eot │ │ ├── data-table.ttf │ │ ├── data-table.woff │ │ ├── fontello │ │ ├── fontello.eot │ │ ├── fontello.ttf │ │ ├── fontello.woff │ │ └── fontello.woff2 │ │ ├── open_sans │ │ ├── OpenSans-Bold.ttf │ │ ├── OpenSans-Italic.ttf │ │ ├── OpenSans-Light.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSans-ExtraBold.ttf │ │ ├── OpenSans-Semibold.ttf │ │ ├── OpenSans-BoldItalic.ttf │ │ ├── OpenSans-LightItalic.ttf │ │ ├── OpenSans-SemiboldItalic.ttf │ │ └── OpenSans-ExtraBoldItalic.ttf │ │ ├── fontello-1afa5ccc │ │ ├── font │ │ │ ├── fontello.eot │ │ │ ├── fontello.ttf │ │ │ ├── fontello.woff │ │ │ └── fontello.woff2 │ │ ├── LICENSE.txt │ │ ├── css │ │ │ ├── fontello-codes.css │ │ │ ├── animation.css │ │ │ ├── fontello-ie7-codes.css │ │ │ ├── fontello-ie7.css │ │ │ └── fontello.css │ │ ├── README.txt │ │ └── config.json │ │ └── data-table.svg ├── app │ ├── index.ts │ ├── shared │ │ ├── utility │ │ │ ├── index.ts │ │ │ ├── utility.module.ts │ │ │ ├── validation.service.ts │ │ │ ├── utility.service.ts │ │ │ └── utilityHelpers.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ ├── auth │ │ │ │ ├── login.model.ts │ │ │ │ ├── register.model.ts │ │ │ │ └── user.model.ts │ │ │ └── product.model.ts │ │ ├── asyncServices │ │ │ └── http │ │ │ │ ├── index.ts │ │ │ │ ├── http.adapter.ts │ │ │ │ ├── http.module.ts │ │ │ │ ├── http.service.ts │ │ │ │ ├── http.decorator.ts │ │ │ │ ├── utils.service.ts │ │ │ │ └── httpResponseHandler.service.ts │ │ ├── containers │ │ │ ├── layout │ │ │ │ ├── layout.container.scss │ │ │ │ ├── layout.sandbox.ts │ │ │ │ └── layout.container.ts │ │ │ └── index.ts │ │ ├── animations │ │ │ ├── index.ts │ │ │ ├── moveInLeft.animation.ts │ │ │ ├── fallIn.animation.ts │ │ │ ├── moveIn.animation.ts │ │ │ ├── slideInRight.animation.ts │ │ │ └── fadeIn.animation.ts │ │ ├── pipes │ │ │ ├── index.ts │ │ │ └── sanitizeHtml.pipe.ts │ │ ├── components │ │ │ ├── navigation │ │ │ │ ├── navigation.component.html │ │ │ │ ├── navigation.component.ts │ │ │ │ └── navigation.component.scss │ │ │ ├── header │ │ │ │ ├── header.component.scss │ │ │ │ └── header.component.ts │ │ │ ├── pageNotFound │ │ │ │ ├── pageNotFound.component.ts │ │ │ │ └── pageNotFound.component.scss │ │ │ ├── profileActionBar │ │ │ │ ├── profileActionBar.component.ts │ │ │ │ └── profileActionBar.component.scss │ │ │ ├── languageSelector │ │ │ │ ├── languageSelector.component.scss │ │ │ │ └── languageSelector.component.ts │ │ │ ├── spinner │ │ │ │ ├── spinner.component.scss │ │ │ │ └── spinner.component.ts │ │ │ ├── index.ts │ │ │ └── loadingPlaceholder │ │ │ │ ├── loadingPlaceholder.component.ts │ │ │ │ └── loadingPlaceholder.component.scss │ │ ├── guards │ │ │ ├── canDeactivate.guard.ts │ │ │ └── auth.guard.ts │ │ ├── store │ │ │ ├── actions │ │ │ │ ├── settings.action.ts │ │ │ │ ├── products.action.ts │ │ │ │ ├── product-details.action.ts │ │ │ │ └── auth.action.ts │ │ │ ├── reducers │ │ │ │ ├── settings.reducer.ts │ │ │ │ ├── products.reducer.ts │ │ │ │ ├── product-details.reducer.ts │ │ │ │ └── auth.reducer.ts │ │ │ ├── effects │ │ │ │ ├── products.effect.ts │ │ │ │ └── auth.effect.ts │ │ │ └── index.ts │ │ └── sandbox │ │ │ └── base.sandbox.ts │ ├── auth │ │ ├── register │ │ │ ├── register.component.scss │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── login │ │ │ ├── login.component.scss │ │ │ ├── login.component.html │ │ │ └── login.component.ts │ │ ├── auth-routing.module.ts │ │ ├── authApiClient.service.ts │ │ ├── auth.module.ts │ │ └── auth.sandbox.ts │ ├── app-routing.module.ts │ ├── products │ │ ├── productsApiClient.service.ts │ │ ├── products.service.ts │ │ ├── products.resolver.ts │ │ ├── products-routing.module.ts │ │ ├── products.module.ts │ │ ├── products.component.ts │ │ ├── products.sandbox.ts │ │ └── product-details.component.ts │ ├── app.component.ts │ ├── app-config.service.ts │ ├── app.sandbox.ts │ └── app.module.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── typings.d.ts ├── main.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── styles │ ├── variables.scss │ ├── paddings.scss │ ├── icons.scss │ └── margins.scss ├── polyfills.ts ├── index.html ├── test.ts └── styles.scss ├── config ├── env.json ├── test.json ├── development.json └── production.json ├── favicon.ico ├── proxy.conf.json ├── e2e ├── app.po.ts ├── app.e2e-spec.ts └── tsconfig.e2e.json ├── .editorconfig ├── index.html ├── i18n ├── components.en.json ├── components.hr.json ├── products.hr.json ├── products.en.json ├── auth.en.json ├── auth.hr.json ├── general.en.json ├── general.hr.json ├── en.json └── hr.json ├── hooks ├── pre-test.js ├── post-build.js ├── pre-start.js └── pre-build.js ├── tsconfig.json ├── sw-precache-config.js ├── .gitignore ├── protractor.conf.js ├── LICENSE.md ├── .angular-cli.json ├── karma.conf.js ├── README.md ├── package.json └── tslint.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": "development" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.component'; 2 | export * from './app.module'; 3 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/Stars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/Stars.png -------------------------------------------------------------------------------- /src/assets/images/Martian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/Martian.png -------------------------------------------------------------------------------- /src/assets/images/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/logout.png -------------------------------------------------------------------------------- /src/assets/fonts/data-table.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/data-table.eot -------------------------------------------------------------------------------- /src/assets/fonts/data-table.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/data-table.ttf -------------------------------------------------------------------------------- /src/assets/fonts/data-table.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/data-table.woff -------------------------------------------------------------------------------- /src/assets/images/users/user.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/users/user.jpg -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://apa.netmedia.hr", 4 | "secure": false, 5 | "changeOrigin": true 6 | } 7 | } -------------------------------------------------------------------------------- /src/assets/images/logo_full_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/logo_full_dark.png -------------------------------------------------------------------------------- /src/assets/images/netmedia-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/netmedia-logo.png -------------------------------------------------------------------------------- /src/assets/fonts/fontello/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello/fontello.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontello/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello/fontello.ttf -------------------------------------------------------------------------------- /src/assets/fonts/fontello/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello/fontello.woff -------------------------------------------------------------------------------- /src/assets/fonts/fontello/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello/fontello.woff2 -------------------------------------------------------------------------------- /src/assets/images/netmedia-logo-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/images/netmedia-logo-grey.png -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-Italic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Typings reference file, you can add your own global typings here 2 | // https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html 3 | -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello-1afa5ccc/font/fontello.eot -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello-1afa5ccc/font/fontello.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-BoldItalic.ttf -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-LightItalic.ttf -------------------------------------------------------------------------------- /src/app/shared/utility/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utility.module'; 2 | export * from './utility.service'; 3 | export * from './utilityHelpers'; 4 | export * from './validation.service'; -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello-1afa5ccc/font/fontello.woff -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/fontello-1afa5ccc/font/fontello.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-SemiboldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-SemiboldItalic.ttf -------------------------------------------------------------------------------- /src/app/shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth/user.model'; 2 | export * from './auth/login.model'; 3 | export * from './auth/register.model'; 4 | export * from './product.model'; -------------------------------------------------------------------------------- /src/assets/fonts/open_sans/OpenSans-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netmedia/angular-architecture-patterns/HEAD/src/assets/fonts/open_sans/OpenSans-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http.service'; 2 | export * from './http.module'; 3 | export * from './http.decorator'; 4 | export * from './httpResponseHandler.service'; -------------------------------------------------------------------------------- /src/app/shared/containers/layout/layout.container.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .layout-content { 4 | padding: 20px 20px 20px 88px; 5 | height: 100%; 6 | background-color: $color-grey-verylight; 7 | } -------------------------------------------------------------------------------- /src/app/shared/animations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fadeIn.animation'; 2 | export * from './slideInRight.animation'; 3 | export * from './moveIn.animation'; 4 | export * from './fallIn.animation'; 5 | export * from './moveInLeft.animation'; -------------------------------------------------------------------------------- /src/app/shared/models/auth/login.model.ts: -------------------------------------------------------------------------------- 1 | export class LoginForm { 2 | public email: string; 3 | public password: string; 4 | 5 | constructor(loginForm: any) { 6 | this.email = loginForm.email || ''; 7 | this.password = loginForm.password || ''; 8 | } 9 | } -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class SystemIntegratorERPPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/app/shared/pipes/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SanitizeHtmlPipe } from './sanitizeHtml.pipe'; 3 | 4 | export const PIPES = [ 5 | SanitizeHtmlPipe 6 | ]; 7 | 8 | @NgModule({ 9 | imports: [], 10 | declarations: PIPES, 11 | exports: PIPES 12 | }) 13 | export class PipesModule { } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Frontend 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /i18n/components.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sidebar": { 3 | "Menu": "Menu", 4 | "ProductsItem": "Products" 5 | }, 6 | 7 | "ProfileActionBar": { 8 | "Logout": "Logout" 9 | }, 10 | 11 | "PageNotFound": { 12 | "Title": "Ooops, Page Not Found", 13 | "Subtitle": "Please, return to the previous page", 14 | "Button": "Go back" 15 | } 16 | } -------------------------------------------------------------------------------- /hooks/pre-test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | function setEnvironment(configPath, environment) { 4 | fs.writeJson(configPath, {env: environment}, 5 | function (res) { 6 | console.log('Environment variable set to ' + environment) 7 | } 8 | ); 9 | } 10 | 11 | // Set environment variable to "test" 12 | setEnvironment('./config/env.json', 'test'); -------------------------------------------------------------------------------- /i18n/components.hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Sidebar": { 3 | "Menu": "Menu", 4 | "ProductsItem": "Proizvodi" 5 | }, 6 | 7 | "ProfileActionBar": { 8 | "Logout": "Odjava" 9 | }, 10 | 11 | "PageNotFound": { 12 | "Title": "Ooops, Stranica Nije Pronađena", 13 | "Subtitle": "Molimo, vratite se na prethodnu stranicu", 14 | "Button": "Povratak" 15 | } 16 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { environment } from './environments/environment'; 4 | import { AppModule } from './app/'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule); 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "lib": [ 12 | "es2016", 13 | "dom" 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /i18n/products.hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Products": { 3 | "Title": "Proizvodi" 4 | }, 5 | "ProductDetails": { 6 | "Title": "Detalji proizvoda", 7 | "Name": "Naziv", 8 | "SerialNumber": "Serijski Broj", 9 | "Category": "Kategorija", 10 | "Description": "Opis", 11 | "WarrantyExpiration": "Istek Garancije", 12 | "Price": "Cijena", 13 | "Currency": "Valuta" 14 | } 15 | } -------------------------------------------------------------------------------- /i18n/products.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Products": { 3 | "Title": "Products" 4 | }, 5 | "ProductDetails": { 6 | "Title": "Product details", 7 | "Name": "Name", 8 | "SerialNumber": "Serial Number", 9 | "Category": "Category", 10 | "Description": "Description", 11 | "WarrantyExpiration": "Warranty Expiration", 12 | "Price": "Price", 13 | "Currency": "Currency" 14 | } 15 | } -------------------------------------------------------------------------------- /src/app/shared/animations/moveInLeft.animation.ts: -------------------------------------------------------------------------------- 1 | import {trigger, state, animate, style, transition} from '@angular/animations'; 2 | 3 | export function moveInLeft() { 4 | return trigger('moveInLeft', [ 5 | transition(':enter', [ 6 | style({opacity:'0', transform: 'translateX(-100px)'}), 7 | animate('.6s .2s ease-in-out', style({opacity:'1', transform: 'translateX(0)'})) 8 | ]) 9 | ]); 10 | } -------------------------------------------------------------------------------- /src/app/shared/models/auth/register.model.ts: -------------------------------------------------------------------------------- 1 | export class RegisterForm { 2 | public email: string; 3 | public password: string; 4 | public confirmPassword: string; 5 | 6 | constructor(registerForm: any) { 7 | this.email = registerForm.email || ''; 8 | this.password = registerForm.password || ''; 9 | this.confirmPassword = registerForm.confirmPassword || ''; 10 | } 11 | } -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { SystemIntegratorERPPage } from './app.po'; 2 | 3 | describe('system-integrator-erp App', function() { 4 | let page: SystemIntegratorERPPage; 5 | 6 | beforeEach(() => { 7 | page = new SystemIntegratorERPPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../dist/out-tsc-e2e", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "types":[ 15 | "jasmine", 16 | "node" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/auth/register/register.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/variables"; 2 | 3 | .register-title { 4 | margin-top: 1.5rem; 5 | text-align: center; 6 | } 7 | 8 | .register-backBtn { 9 | font-weight: bold; 10 | text-transform: uppercase; 11 | font-size: .9em; 12 | color: $color-theme-blue; 13 | 14 | &:hover { 15 | color: $color-grey; 16 | } 17 | } 18 | 19 | .register-submitBtn { 20 | width: 100%; 21 | padding: 1em; 22 | } -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/http.adapter.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '@angular/http'; 2 | 3 | export class HttpAdapter { 4 | 5 | static baseAdapter(res: Response, adapterFn?: Function): any { 6 | if (res.status === 200) { 7 | try { 8 | let jsonRes = res.json(); 9 | return adapterFn ? adapterFn.call(undefined, jsonRes) : jsonRes; 10 | } catch (e) { 11 | return res; 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Font Awesome 5 | 6 | Copyright (C) 2016 by Dave Gandy 7 | 8 | Author: Dave Gandy 9 | License: SIL () 10 | Homepage: http://fortawesome.github.com/Font-Awesome/ 11 | 12 | 13 | ## Entypo 14 | 15 | Copyright (C) 2012 by Daniel Bruce 16 | 17 | Author: Daniel Bruce 18 | License: SIL (http://scripts.sil.org/OFL) 19 | Homepage: http://www.entypo.com 20 | 21 | 22 | -------------------------------------------------------------------------------- /sw-precache-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | navigateFallback: '/index.html', 3 | stripPrefix: './dist', 4 | root: './dist/', 5 | staticFileGlobs: [ 6 | './dist/index.html', 7 | './dist/**.js', 8 | './dist/**.css', 9 | './dist/**.ttf', 10 | './dist/assets/images/*', 11 | './dist/config/*', 12 | './dist/i18n/en.json', 13 | './dist/i18n/hr.json' 14 | ], 15 | runtimeCaching: [{ 16 | urlPattern: '', 17 | handler: 'fastest' 18 | }] 19 | }; -------------------------------------------------------------------------------- /src/app/shared/pipes/sanitizeHtml.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Pipe, 3 | PipeTransform 4 | } from '@angular/core'; 5 | import { 6 | DomSanitizer, 7 | SafeHtml 8 | } from '@angular/platform-browser'; 9 | 10 | @Pipe({ 11 | name: 'sanitizeHtml' 12 | }) 13 | export class SanitizeHtmlPipe implements PipeTransform { 14 | constructor(private sanitizer: DomSanitizer){} 15 | 16 | transform(v: string) : SafeHtml { 17 | return this.sanitizer.bypassSecurityTrustHtml(v); 18 | } 19 | } -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/app", 13 | "target": "es5", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [] 17 | }, 18 | "exclude": [ 19 | "test.ts", 20 | "**/*.spec.ts" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/app/shared/utility/utility.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NgModule, 3 | ModuleWithProviders 4 | } from "@angular/core"; 5 | import { UtilService } from './utility.service'; 6 | import { ValidationService } from './validation.service'; 7 | 8 | @NgModule() 9 | export class UtilityModule { 10 | static forRoot(): ModuleWithProviders { 11 | return { 12 | ngModule: UtilityModule, 13 | 14 | providers: [ 15 | UtilService, 16 | ValidationService 17 | ] 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | /.vscode 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | 19 | # misc 20 | /.sass-cache 21 | /connect.lock 22 | /coverage/* 23 | /libpeerconnection.log 24 | npm-debug.log 25 | testem.log 26 | /typings 27 | 28 | # e2e 29 | /e2e/*.js 30 | /e2e/*.map 31 | 32 | #System Files 33 | .DS_Store 34 | Thumbs.db 35 | -------------------------------------------------------------------------------- /src/app/shared/components/navigation/navigation.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hooks/post-build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var foldersToCopy = [ 3 | {src: './config', dest: './dist/config'}, 4 | {src: './i18n', dest: './dist/i18n'} 5 | ]; 6 | 7 | // copies directory, even if it has subdirectories or files 8 | function copyDir(src, dest) { 9 | fs.copy(src, dest, function (err) { 10 | if (err) return console.error(err) 11 | console.log(src + ' folder successfully copied') 12 | }); 13 | } 14 | 15 | for (var i = foldersToCopy.length - 1; i >= 0; i--) { 16 | copyDir(foldersToCopy[i].src, foldersToCopy[i].dest); 17 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2016" 10 | ], 11 | "outDir": "../out-tsc/spec", 12 | "module": "commonjs", 13 | "target": "es6", 14 | "baseUrl": "", 15 | "types": [ 16 | "jasmine", 17 | "node" 18 | ] 19 | }, 20 | "files": [ 21 | "test.ts" 22 | ], 23 | "include": [ 24 | "**/*.spec.ts" 25 | ] 26 | } -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { PageNotFoundComponent } from './shared/components/pageNotFound/pageNotFound.component'; 4 | 5 | const appRoutes: Routes = [ 6 | { path: '', redirectTo: '/products', pathMatch: 'full' }, 7 | { path: '**', component: PageNotFoundComponent } 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [ 12 | RouterModule.forRoot(appRoutes) 13 | ], 14 | exports: [ 15 | RouterModule 16 | ] 17 | }) 18 | export class AppRoutingModule {} -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | /* Colors definition */ 2 | $color-black: #000; 3 | $color-white: #fff; 4 | $color-grey-ultralight: #f9f9f9; 5 | $color-grey-verylight: #ebebeb; 6 | $color-grey-light: #ccc; 7 | $color-grey: #869ba6; 8 | $color-grey-dark: #6d6f70; 9 | $color-grey-verydark: #333; 10 | $color-red: #ee5a5a; 11 | $color-redLight: #f57676; 12 | 13 | /* Site specific */ 14 | $color-theme-black: #2f2f30; 15 | $color-theme-grey: #DEE9EE; 16 | $color-theme-blue: #87bcd3; 17 | $color-theme-lightBlue: #def5ff; 18 | $color-theme-violet: #353535; 19 | $color-theme-darkviolet: #2a2a2a; 20 | -------------------------------------------------------------------------------- /src/app/auth/login/login.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles/variables"; 2 | 3 | .login-label { 4 | text-align: center; 5 | margin-bottom: 10px; 6 | font-size: 16px; 7 | } 8 | 9 | .login-title { 10 | margin-top: 0; 11 | text-align: center; 12 | } 13 | 14 | .login-registerLink { 15 | margin-top: 15px; 16 | font-weight: bold; 17 | text-align: center; 18 | display: block; 19 | font-size: 1.2em; 20 | color: $color-theme-blue; 21 | 22 | &:hover { 23 | color: $color-grey; 24 | } 25 | } 26 | 27 | .login-submitBtn { 28 | width: 100%; 29 | padding: 1em; 30 | } -------------------------------------------------------------------------------- /src/app/shared/guards/canDeactivate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanDeactivate } from '@angular/router'; 3 | import { Observable } from 'rxjs/Observable'; 4 | 5 | export interface CanComponentDeactivate { 6 | canDeactivate: () => Observable | Promise | boolean; 7 | } 8 | 9 | @Injectable() 10 | export class CanDeactivateGuard implements CanDeactivate { 11 | canDeactivate(component: CanComponentDeactivate) { 12 | return component.canDeactivate ? component.canDeactivate() : true; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/shared/animations/fallIn.animation.ts: -------------------------------------------------------------------------------- 1 | import {trigger, state, animate, style, transition} from '@angular/animations'; 2 | 3 | export function fallIn() { 4 | return trigger('fallIn', [ 5 | transition(':enter', [ 6 | style({opacity:'0', transform: 'translateY(40px)'}), 7 | animate('.4s .2s ease-in-out', style({opacity:'1', transform: 'translateY(0)'})) 8 | ]), 9 | transition(':leave', [ 10 | style({opacity:'1', transform: 'translateX(0)'}), 11 | animate('.3s ease-in-out', style({opacity:'0', transform: 'translateX(-200px)'})) 12 | ]) 13 | ]); 14 | } -------------------------------------------------------------------------------- /src/app/shared/models/auth/user.model.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | 3 | public email: string; 4 | public isLoggedIn: boolean; 5 | 6 | constructor(user?: any) { 7 | this.email = user ? user.email : ''; 8 | this.isLoggedIn = this.email ? true : false; 9 | } 10 | 11 | /** 12 | * Saves user into local storage 13 | * 14 | * @param user 15 | */ 16 | public save(): void { 17 | localStorage.setItem('currentUser', JSON.stringify(this)); 18 | } 19 | 20 | /** 21 | * Saves user into local storage 22 | */ 23 | public remove(): void { 24 | localStorage.setItem('currentUser', null); 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/http.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from "@angular/common"; 2 | import { 3 | NgModule, 4 | ModuleWithProviders 5 | } from "@angular/core"; 6 | import { HttpService } from './http.service'; 7 | import { HttpResponseHandler } from './httpResponseHandler.service'; 8 | 9 | @NgModule({ 10 | imports: [CommonModule] 11 | }) 12 | export class HttpServiceModule { 13 | static forRoot(): ModuleWithProviders { 14 | return { 15 | ngModule: HttpServiceModule, 16 | 17 | providers: [ 18 | HttpService, 19 | HttpResponseHandler 20 | ] 21 | }; 22 | } 23 | } -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/app/shared/containers/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ComponentsModule } from '../components'; 5 | import { LayoutContainer } from './layout/layout.container'; 6 | import { LayoutSandbox } from './layout/layout.sandbox'; 7 | import { TranslateModule } from 'ng2-translate'; 8 | 9 | export const CONTAINERS = [ 10 | LayoutContainer 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | ComponentsModule, 17 | TranslateModule 18 | ], 19 | declarations: CONTAINERS, 20 | exports: CONTAINERS, 21 | providers: [LayoutSandbox] 22 | }) 23 | export class ContainersModule { } -------------------------------------------------------------------------------- /src/app/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | RouterModule, 4 | Routes 5 | } from '@angular/router'; 6 | import { LoginComponent } from './login/login.component'; 7 | import { RegisterComponent } from './register/register.component'; 8 | 9 | const authRoutes: Routes = [ 10 | { 11 | path: 'login', 12 | component: LoginComponent 13 | }, 14 | { 15 | path: 'register', 16 | component: RegisterComponent 17 | } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [ 22 | RouterModule.forChild(authRoutes) 23 | ], 24 | exports: [ 25 | RouterModule 26 | ] 27 | }) 28 | export class AuthRoutingModule { } -------------------------------------------------------------------------------- /src/app/shared/store/actions/settings.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { type } from '../../utility'; 3 | 4 | export const ActionTypes = { 5 | SET_LANGUAGE: type('[Settings] SetLanguage'), 6 | SET_CULTURE: type('[Settings] SetCulture') 7 | }; 8 | 9 | /** 10 | * Settings Actions 11 | */ 12 | export class SetLanguageAction implements Action { 13 | type = ActionTypes.SET_LANGUAGE; 14 | 15 | constructor(public payload: string) { } 16 | } 17 | 18 | export class SetCultureAction implements Action { 19 | type = ActionTypes.SET_CULTURE; 20 | 21 | constructor(public payload: string) { } 22 | } 23 | 24 | export type Actions 25 | = SetLanguageAction 26 | | SetCultureAction; -------------------------------------------------------------------------------- /src/app/shared/animations/moveIn.animation.ts: -------------------------------------------------------------------------------- 1 | import {trigger, state, animate, style, transition} from '@angular/animations'; 2 | 3 | export function moveIn() { 4 | return trigger('moveIn', [ 5 | state('void', style({position: 'fixed', width: '100%'}) ), 6 | state('*', style({position: 'fixed', width: '100%'}) ), 7 | transition(':enter', [ 8 | style({opacity:'0', transform: 'translateX(100px)'}), 9 | animate('.6s ease-in-out', style({opacity:'1', transform: 'translateX(0)'})) 10 | ]), 11 | transition(':leave', [ 12 | style({opacity:'1', transform: 'translateX(0)'}), 13 | animate('.3s ease-in-out', style({opacity:'0', transform: 'translateX(-200px)'})) 14 | ]) 15 | ]); 16 | } -------------------------------------------------------------------------------- /src/app/shared/animations/slideInRight.animation.ts: -------------------------------------------------------------------------------- 1 | import { AnimationEntryMetadata } from '@angular/core'; 2 | import { animate, state, style, transition, trigger } from '@angular/animations'; 3 | 4 | // Component transition animations 5 | export const slideInRightAnimation: AnimationEntryMetadata = 6 | trigger('slideInRightAnimation', [ 7 | state('in', style({opacity: 1, transform: 'translateX(0)'})), 8 | transition('void => *', [ 9 | style({ 10 | opacity: 0, 11 | transform: 'translateX(100%)' 12 | }), 13 | animate('0.2s ease-in') 14 | ]), 15 | transition('* => void', [ 16 | animate('0.2s 10 ease-out', style({ 17 | opacity: 0, 18 | transform: 'translateX(100%)' 19 | })) 20 | ]) 21 | ]); -------------------------------------------------------------------------------- /i18n/auth.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Auth": { 3 | "Email": "Email", 4 | "Password": "Password", 5 | "ConfirmPassword": "Confirm Password", 6 | "EmailFormatError": "Email is not in valid format", 7 | "EmailRequiredError": "Email is required", 8 | "PasswordRequiredError": "Password is required", 9 | "ConfirmPasswordError": "Passwords mismatch", 10 | "PasswordLengthError": "Passwords must contain at least 6 characters", 11 | 12 | "Login": { 13 | "Title": "Login", 14 | "Submit": "Login", 15 | "RegisterLink": "Sign Up" 16 | }, 17 | 18 | "Register": { 19 | "Title": "Sign Up", 20 | "Submit": "Create account", 21 | "Back": "Go Back", 22 | "SuccessMessage": "Your account has been successfully created" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /i18n/auth.hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Auth": { 3 | "Email": "Email", 4 | "Password": "Lozinka", 5 | "ConfirmPassword": "Potvrdi lozinku", 6 | "EmailFormatError": "Email adresa nije u pravilnom formatu", 7 | "EmailRequiredError": "Email adresa je obavezna", 8 | "PasswordRequiredError": "Lozinka je obavezna", 9 | "ConfirmPasswordError": "Lozinke se ne podudaraju", 10 | "PasswordLengthError": "Lozinka mora sadržavati najmanje 6 znakova", 11 | 12 | "Login": { 13 | "Title": "Prijavi se", 14 | "Submit": "Prijavi se", 15 | "RegisterLink": "Napravi račun" 16 | }, 17 | 18 | "Register": { 19 | "Title": "Napravi račun", 20 | "Submit": "Napravi račun", 21 | "Back": "Natrag", 22 | "SuccessMessage": "Vaš račun je uspješno kreiran" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .header { 4 | position: fixed; 5 | left: 0; 6 | top: 0; 7 | z-index: 1000; 8 | width: 100%; 9 | height: 70px; 10 | padding: 0 20px; 11 | background: $color-white; 12 | -webkit-box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.24); 13 | -moz-box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.24); 14 | box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.24); 15 | } 16 | 17 | .header-logo { 18 | display: inline-block; 19 | height: 100%; 20 | padding: 15px 0; 21 | 22 | img { 23 | height: 100%; 24 | } 25 | } 26 | 27 | .header-profileBarWrapper { 28 | float: right; 29 | height: 100%; 30 | } 31 | 32 | .header-languageSelectorWrapper { 33 | float: right; 34 | margin-top: 20px; 35 | margin-right: 20px; 36 | } -------------------------------------------------------------------------------- /src/app/shared/components/navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectorRef, 4 | ChangeDetectionStrategy } from '@angular/core'; 5 | import { TranslateService } from 'ng2-translate'; 6 | 7 | @Component({ 8 | selector: 'navigation', 9 | templateUrl: './navigation.component.html', 10 | styleUrls: ['./navigation.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class NavigationComponent { 14 | 15 | constructor(private changeDetector: ChangeDetectorRef, private translate: TranslateService) { 16 | 17 | /** 18 | * Detaches the change detector from the change detector tree. 19 | * The detached change detector will not be checked until it is reattached. 20 | */ 21 | // changeDetector.detach(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /i18n/general.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Access not allowed. Please login.", 3 | "ServerError403": "Access forbidden. Please provide correct credentials", 4 | "ServerError404": "An error occurred. Please contact your administrator", 5 | "ServerError500": "An error occurred. Please contact your administrator", 6 | "SuccessNotificationTitle": "Success", 7 | "ErrorNotificationTitle": "Error", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Warning", 10 | "SaveBtn": "Save", 11 | "EditBtn": "Edit", 12 | "CancelBtn": "Cancel", 13 | "BackBtn": "Back", 14 | "GridEmptyLabel": "No available items", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Please confirm", 18 | "Content": "Do you want to leave without saving the changes?", 19 | "SubmitBtn": "Yes", 20 | "CancelBtn": "No" 21 | } 22 | } -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [ 13 | {"code": "hr", "name": "HR", "culture": "hr-HR"}, 14 | {"code": "en", "name": "EN", "culture": "en-EN"} 15 | ], 16 | "defaultLanguage": "hr" 17 | }, 18 | 19 | "notifications": { 20 | "options": { 21 | "timeOut": 5000, 22 | "showProgressBar": true, 23 | "pauseOnHover": true, 24 | "position": ["top", "right"], 25 | "theClass": "sy-notification" 26 | }, 27 | "unauthorizedEndpoints": ["api/products"], 28 | "notFoundEndpoints": ["api/products", "api/account/login", "api/account/register"] 29 | }, 30 | 31 | "debugging": true 32 | } -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [ 13 | {"code": "hr", "name": "HR", "culture": "hr-HR"}, 14 | {"code": "en", "name": "EN", "culture": "en-EN"} 15 | ], 16 | "defaultLanguage": "hr" 17 | }, 18 | 19 | "notifications": { 20 | "options": { 21 | "timeOut": 5000, 22 | "showProgressBar": true, 23 | "pauseOnHover": true, 24 | "position": ["top", "right"], 25 | "theClass": "sy-notification" 26 | }, 27 | "unauthorizedEndpoints": ["api/products"], 28 | "notFoundEndpoints": ["api/products", "api/account/login", "api/account/register"] 29 | }, 30 | 31 | "debugging": true 32 | } -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "api": { 3 | "baseUrl": "/api" 4 | }, 5 | 6 | "paths": { 7 | "imagesRoot": "/assets/images/", 8 | "userImageFolder": "/assets/images/users/" 9 | }, 10 | 11 | "localization": { 12 | "languages": [ 13 | {"code": "hr", "name": "HR", "culture": "hr-HR"}, 14 | {"code": "en", "name": "EN", "culture": "en-EN"} 15 | ], 16 | "defaultLanguage": "hr" 17 | }, 18 | 19 | "notifications": { 20 | "options": { 21 | "timeOut": 5000, 22 | "showProgressBar": true, 23 | "pauseOnHover": true, 24 | "position": ["top", "right"], 25 | "theClass": "sy-notification" 26 | }, 27 | "unauthorizedEndpoints": ["api/products"], 28 | "notFoundEndpoints": ["api/products", "api/account/login", "api/account/register"] 29 | }, 30 | 31 | "debugging": false 32 | } -------------------------------------------------------------------------------- /src/app/shared/guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | CanActivate, Router, 4 | ActivatedRouteSnapshot, 5 | RouterStateSnapshot 6 | } from '@angular/router'; 7 | import { Observable } from "rxjs/Rx"; 8 | 9 | @Injectable() 10 | export class AuthGuard implements CanActivate { 11 | constructor(private router: Router) {} 12 | 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { 14 | return this.checkLogin(state.url); 15 | } 16 | 17 | checkLogin(url: string): boolean { 18 | var currentUser = JSON.parse(localStorage.getItem('currentUser')); 19 | if (currentUser) return true; 20 | 21 | // Navigate to the login page with extras 22 | this.router.navigate(['/login']); 23 | return false; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Architecture Patterns 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 24 | 25 | -------------------------------------------------------------------------------- /i18n/general.hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Pristup nije dozvoljen. Molimo prijavite se.", 3 | "ServerError403": "Pristup nije dozvoljen. Molimo unesite ispravne podatke.", 4 | "ServerError404": "Dogodila se greška. Molimo kontaktirajte svog administratora.", 5 | "ServerError500": "Dogodila se greška. Molimo kontaktirajte svog administratora.", 6 | "SuccessNotificationTitle": "Uspješno", 7 | "ErrorNotificationTitle": "Greška", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Upozorenje", 10 | "SaveBtn": "Spremi", 11 | "EditBtn": "Uredi", 12 | "CancelBtn": "Odustani", 13 | "BackBtn": "Nazad", 14 | "GridEmptyLabel": "Nema dostupnih rezultata", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Molimo potvrdite", 18 | "Content": "Da li želite zatvoriti formu bez spremanja promjena?", 19 | "SubmitBtn": "Da", 20 | "CancelBtn": "Ne" 21 | } 22 | } -------------------------------------------------------------------------------- /src/app/products/productsApiClient.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpService, 4 | GET, 5 | Path, 6 | Adapter 7 | } from '../shared/asyncServices/http'; 8 | import { Observable } from 'rxjs/Observable'; 9 | import { ProductsService } from './products.service'; 10 | 11 | @Injectable() 12 | export class ProductsApiClient extends HttpService { 13 | 14 | /** 15 | * Retrieves all products 16 | */ 17 | @GET("/product") 18 | @Adapter(ProductsService.gridAdapter) 19 | public getProducts(): Observable { return null; }; 20 | 21 | /** 22 | * Retrieves product details by a given id 23 | * 24 | * @param id 25 | */ 26 | @GET("/product/{id}") 27 | @Adapter(ProductsService.productDetailsAdapter) 28 | public getProductDetails(@Path("id") id: number): Observable { return null; }; 29 | } -------------------------------------------------------------------------------- /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 | /*global jasmine */ 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | beforeLaunch: function() { 24 | require('ts-node').register({ 25 | project: 'e2e' 26 | }); 27 | }, 28 | onPrepare() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/products/products.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | Inject, 4 | forwardRef 5 | } from '@angular/core'; 6 | import { Product } from '../shared/models'; 7 | import { ProductsSandbox } from './products.sandbox'; 8 | 9 | @Injectable() 10 | export class ProductsService { 11 | 12 | private productsSubscription; 13 | 14 | /** 15 | * Transforms grid data products recieved from the API into array of 'Product' instances 16 | * 17 | * @param products 18 | */ 19 | static gridAdapter(products: any): Array { 20 | return products.map(product => new Product(product)); 21 | } 22 | 23 | /** 24 | * Transforms product details recieved from the API into instance of 'Product' 25 | * 26 | * @param product 27 | */ 28 | static productDetailsAdapter(product: any): Product { 29 | return new Product(product); 30 | } 31 | } -------------------------------------------------------------------------------- /src/app/shared/components/pageNotFound/pageNotFound.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ChangeDetectionStrategy } from '@angular/core'; 2 | import { Location } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'page-not-found', 6 | template: ` 7 |
8 |
9 | 10 |

{{ 'PageNotFound.Title' | translate }}

11 |

{{ 'PageNotFound.Subtitle' | translate }}

12 | 13 |
14 |
15 | `, 16 | styleUrls: ['./pageNotFound.component.scss'], 17 | changeDetection: ChangeDetectionStrategy.OnPush 18 | }) 19 | export class PageNotFoundComponent { 20 | constructor(private location: Location) {} 21 | 22 | public goBack() { 23 | this.location.back(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/shared/animations/fadeIn.animation.ts: -------------------------------------------------------------------------------- 1 | import { AnimationEntryMetadata } from '@angular/core'; 2 | import { animate, state, style, transition, trigger } from '@angular/animations'; 3 | 4 | export const fadeInAnimation: AnimationEntryMetadata = 5 | trigger('fadeInAnimation', [ 6 | state('true' , style({ opacity: 1 })), 7 | state('false', style({ opacity: 0 })), 8 | transition('1 => 0', animate('100ms')), 9 | transition('0 => 1', animate('250ms')) 10 | ]); 11 | // trigger('fadeInAnimation', [ 12 | // transition('void => *', [ 13 | // style({opacity:0}), //style only for transition transition (after transiton it removes) 14 | // animate(100, style({opacity:1})) // the new state of the transition(after transiton it removes) 15 | // ]), 16 | // transition('* => void', [ 17 | // animate(100, style({opacity:0})) // the new state of the transition(after transiton it removes) 18 | // ]) 19 | // ]); -------------------------------------------------------------------------------- /src/app/shared/components/profileActionBar/profileActionBar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter, ChangeDetectionStrategy, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'profile-action-bar', 5 | template: ` 6 |
7 |
8 | 9 | {{ userEmail }} | 10 | {{ 'ProfileActionBar.Logout' | translate }} 11 |
12 |
13 | `, 14 | styleUrls: ['./profileActionBar.component.scss'], 15 | changeDetection: ChangeDetectionStrategy.OnPush 16 | }) 17 | export class ProfileActionBarComponent { 18 | 19 | @Input() userImage: string; 20 | @Input() userEmail: string; 21 | @Output() logout: EventEmitter = new EventEmitter(); 22 | 23 | constructor() {} 24 | } -------------------------------------------------------------------------------- /src/app/shared/store/actions/products.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Product } from '../../models'; 3 | import { type } from '../../utility'; 4 | 5 | export const ActionTypes = { 6 | LOAD: type('[Products] Load'), 7 | LOAD_SUCCESS: type('[Products] Load Success'), 8 | LOAD_FAIL: type('[Products] Load Fail') 9 | }; 10 | 11 | /** 12 | * Product Actions 13 | */ 14 | export class LoadAction implements Action { 15 | type = ActionTypes.LOAD; 16 | 17 | constructor(public payload: any = null) { } 18 | } 19 | 20 | export class LoadSuccessAction implements Action { 21 | type = ActionTypes.LOAD_SUCCESS; 22 | 23 | constructor(public payload: Array) { } 24 | } 25 | 26 | export class LoadFailAction implements Action { 27 | type = ActionTypes.LOAD_FAIL; 28 | 29 | constructor(public payload: any = null) { } 30 | } 31 | 32 | export type Actions 33 | = LoadAction 34 | | LoadSuccessAction 35 | | LoadFailAction; -------------------------------------------------------------------------------- /src/app/shared/models/product.model.ts: -------------------------------------------------------------------------------- 1 | export class Product { 2 | public id: number; 3 | public serialNumber: string; 4 | public name: string; 5 | public description: string; 6 | public category: string; 7 | public warrantyExpiration: string; 8 | public price: number; 9 | public currency: string; 10 | 11 | constructor(product: any = null) { 12 | this.id = product ? product.Id : null; 13 | this.serialNumber = product ? product.SerialNumber : ''; 14 | this.name = product ? product.Name : ''; 15 | this.description = product ? product.Description : ''; 16 | this.category = product ? product.Category : ''; 17 | this.warrantyExpiration = product ? product.WarrantyExpiration : ''; 18 | this.price = product ? product.Price : null; 19 | this.currency = product ? product.Currency : ''; 20 | } 21 | } -------------------------------------------------------------------------------- /src/app/shared/store/actions/product-details.action.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | import { Product } from '../../models'; 3 | import { type } from '../../utility'; 4 | 5 | export const ActionTypes = { 6 | LOAD: type('[Product Details] Load'), 7 | LOAD_SUCCESS: type('[Product Details] Load Success'), 8 | LOAD_FAIL: type('[Product Details] Load Fail') 9 | }; 10 | 11 | /** 12 | * Product Actions 13 | */ 14 | export class LoadAction implements Action { 15 | type = ActionTypes.LOAD; 16 | 17 | constructor(public payload: number = null) { } 18 | } 19 | 20 | export class LoadSuccessAction implements Action { 21 | type = ActionTypes.LOAD_SUCCESS; 22 | 23 | constructor(public payload: Product) { } 24 | } 25 | 26 | export class LoadFailAction implements Action { 27 | type = ActionTypes.LOAD_FAIL; 28 | 29 | constructor(public payload: any = null) { } 30 | } 31 | 32 | export type Actions 33 | = LoadAction 34 | | LoadSuccessAction 35 | | LoadFailAction; -------------------------------------------------------------------------------- /src/app/products/products.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | Resolve, 4 | ActivatedRouteSnapshot 5 | } from '@angular/router'; 6 | import { ProductsSandbox } from './products.sandbox'; 7 | 8 | @Injectable() 9 | export class ProductsResolver implements Resolve { 10 | 11 | private productsSubscription; 12 | 13 | constructor(public productsSandbox: ProductsSandbox) {} 14 | 15 | /** 16 | * Triggered when application hits product details route. 17 | * It subscribes to product list data and finds one with id from the route params. 18 | * 19 | * @param route 20 | */ 21 | public resolve(route: ActivatedRouteSnapshot) { 22 | if (this.productsSubscription) return; 23 | 24 | this.productsSubscription = this.productsSandbox.productDetails$.subscribe(product => { 25 | if (!product) { 26 | this.productsSandbox.loadProductDetails(parseInt(route.params.id)); 27 | return; 28 | } 29 | 30 | this.productsSandbox.selectProduct(product); 31 | }); 32 | } 33 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 NET Media international services ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/long-stack-trace-zone'; 2 | import 'zone.js/dist/proxy.js'; 3 | import 'zone.js/dist/sync-test'; 4 | import 'zone.js/dist/jasmine-patch'; 5 | import 'zone.js/dist/async-test'; 6 | import 'zone.js/dist/fake-async-test'; 7 | import { getTestBed } from '@angular/core/testing'; 8 | import { 9 | BrowserDynamicTestingModule, 10 | platformBrowserDynamicTesting 11 | } from '@angular/platform-browser-dynamic/testing'; 12 | 13 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 14 | declare var __karma__: any; 15 | declare var require: any; 16 | 17 | // Prevent Karma from running prematurely. 18 | __karma__.loaded = function () {}; 19 | 20 | // First, initialize the Angular testing environment. 21 | getTestBed().initTestEnvironment( 22 | BrowserDynamicTestingModule, 23 | platformBrowserDynamicTesting() 24 | ); 25 | // Then we find all the tests. 26 | const context = require.context('./', true, /\.spec\.ts$/); 27 | // And load the modules. 28 | context.keys().map(context); 29 | // Finally, start Karma to run the tests. 30 | __karma__.start(); 31 | -------------------------------------------------------------------------------- /src/app/shared/components/languageSelector/languageSelector.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | :host { 4 | position: relative; 5 | float: left; 6 | width: 100%; 7 | } 8 | 9 | .dropdown-pane { 10 | top: 35px; 11 | border-radius: 5px; 12 | visibility: visible; 13 | width: 65px; 14 | padding: 0; 15 | 16 | span { 17 | display: block; 18 | width: 100%; 19 | text-align: center; 20 | cursor: pointer; 21 | padding: 0.5rem; 22 | color: $color-grey; 23 | 24 | &:first-child { 25 | margin-bottom: 5px; 26 | } 27 | 28 | &:hover { 29 | background-color: $color-theme-lightBlue; 30 | } 31 | } 32 | } 33 | 34 | .language-selector-anchor { 35 | display: block; 36 | background-color: $color-white; 37 | border: 1px solid $color-grey-light; 38 | border-radius: 5px; 39 | color: $color-grey; 40 | padding: 7px 20px; 41 | outline: none; 42 | width: 65px; 43 | text-transform: uppercase; 44 | font-size: 16px; 45 | 46 | &:hover, 47 | &:active, 48 | &:focus { 49 | text-decoration:none; 50 | background-color: $color-theme-lightBlue; 51 | } 52 | } -------------------------------------------------------------------------------- /src/app/products/products-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { AuthGuard } from '../shared/guards/auth.guard'; 4 | import { CanDeactivateGuard } from '../shared/guards/canDeactivate.guard'; 5 | import { ProductsComponent } from './products.component'; 6 | import { ProductDetailsComponent } from './product-details.component'; 7 | import { ProductsResolver } from './products.resolver'; 8 | 9 | const productsRoutes: Routes = [ 10 | { 11 | path: 'products', 12 | component: ProductsComponent, 13 | data: { 14 | name: 'product-list' 15 | }, 16 | canActivate: [AuthGuard] 17 | }, 18 | { 19 | path: 'products/:id', 20 | component: ProductDetailsComponent, 21 | resolve: { 22 | productDetails: ProductsResolver 23 | }, 24 | canActivate: [AuthGuard] 25 | }, 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [ 30 | RouterModule.forChild(productsRoutes) 31 | ], 32 | exports: [ 33 | RouterModule 34 | ] 35 | }) 36 | export class ProductsRoutingModule { } 37 | -------------------------------------------------------------------------------- /hooks/pre-start.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var jsonConcat = require("json-concat"); 3 | 4 | var localizationSourceFilesEN = [ 5 | "./i18n/general.en.json", 6 | "./i18n/auth.en.json", 7 | "./i18n/products.en.json", 8 | "./i18n/components.en.json" 9 | ]; 10 | 11 | var localizationSourceFilesHR = [ 12 | "./i18n/general.hr.json", 13 | "./i18n/auth.hr.json", 14 | "./i18n/products.hr.json", 15 | "./i18n/components.hr.json" 16 | ]; 17 | 18 | function mergeAndSaveJsonFiles(src, dest) { 19 | jsonConcat({ src: src, dest: dest }, 20 | function (res) { 21 | console.log('Localization files successfully merged!'); 22 | } 23 | ); 24 | } 25 | 26 | function setEnvironment(configPath, environment) { 27 | fs.writeJson(configPath, {env: environment}, 28 | function (res) { 29 | console.log('Environment variable set to ' + environment) 30 | } 31 | ); 32 | } 33 | 34 | // Set environment variable to "development" 35 | setEnvironment('./config/env.json', 'development'); 36 | 37 | // Merge all localization files into one 38 | mergeAndSaveJsonFiles(localizationSourceFilesEN, "./i18n/en.json"); 39 | mergeAndSaveJsonFiles(localizationSourceFilesHR, "./i18n/hr.json"); -------------------------------------------------------------------------------- /hooks/pre-build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | var jsonConcat = require("json-concat"); 3 | 4 | var localizationSourceFilesEN = [ 5 | "./i18n/general.en.json", 6 | "src/i18n/auth.en.json", 7 | "./i18n/products.en.json", 8 | "./i18n/components.en.json" 9 | ]; 10 | 11 | var localizationSourceFilesHR = [ 12 | "./i18n/general.hr.json", 13 | "./i18n/auth.hr.json", 14 | "./i18n/products.hr.json", 15 | "./i18n/components.hr.json" 16 | ]; 17 | 18 | function mergeAndSaveJsonFiles(src, dest) { 19 | jsonConcat({ src: src, dest: dest }, 20 | function (res) { 21 | console.log('Localization files successfully merged!'); 22 | } 23 | ); 24 | } 25 | 26 | function setEnvironment(configPath, environment) { 27 | fs.writeJson(configPath, {env: environment}, 28 | function (res) { 29 | console.log('Environment variable set to ' + environment) 30 | } 31 | ); 32 | } 33 | 34 | // Set environment variable to "production" 35 | setEnvironment('./src/config/env.json', 'production'); 36 | 37 | // Merge all localization files into one 38 | mergeAndSaveJsonFiles(localizationSourceFilesEN, "./src/i18n/en.json"); 39 | mergeAndSaveJsonFiles(localizationSourceFilesHR, "./src/i18n/hr.json"); -------------------------------------------------------------------------------- /src/app/shared/sandbox/base.sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import * as store from '../store'; 5 | import * as authActions from '../store/actions/auth.action'; 6 | import { User } from '../models'; 7 | import { localeDateString } from '../utility'; 8 | 9 | export abstract class Sandbox { 10 | 11 | public loggedUser$: Observable = this.appState$.select(store.getLoggedUser); 12 | public culture$: Observable = this.appState$.select(store.getSelectedCulture); 13 | public culture: string; 14 | 15 | constructor(protected appState$: Store) {} 16 | 17 | /** 18 | * Pulls user from local storage and saves it to the store 19 | */ 20 | public loadUser(): void { 21 | var user = JSON.parse(localStorage.getItem('currentUser')); 22 | this.appState$.dispatch(new authActions.AddUserAction(new User(user))); 23 | } 24 | 25 | /** 26 | * Formats date string based on selected culture 27 | * 28 | * @param value 29 | */ 30 | public formatDate(value: string) { 31 | return localeDateString(value, this.culture); 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { AppSandbox } from './app.sandbox'; 4 | 5 | @Component({ 6 | selector: 'body', 7 | template: ` 8 | 9 | 10 | `, 11 | host: {'[class.body-loginPage]':'isLoginPage'}, 12 | providers: [AppSandbox] 13 | }) 14 | export class AppComponent { 15 | 16 | public isLoginPage: boolean; 17 | 18 | constructor( 19 | private router: Router, 20 | public appSandbox: AppSandbox 21 | ) {} 22 | 23 | ngOnInit() { 24 | this.appSandbox.setupLanguage(); 25 | // Load user from local storage into redux state 26 | this.appSandbox.loadUser(); 27 | this.registerEvents(); 28 | } 29 | 30 | /** 31 | * Registers events needed for the application 32 | */ 33 | private registerEvents(): void { 34 | // Subscribes to route change event and sets "isLoginPage" variable in order to set correct CSS class on body tag. 35 | this.router.events.subscribe((route) => { 36 | this.isLoginPage = route['url'] === '/login' ? true : false; 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/auth/authApiClient.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { 4 | LoginForm, 5 | RegisterForm 6 | } from '../shared/models'; 7 | import { 8 | HttpService, 9 | POST, 10 | Body, 11 | DefaultHeaders, 12 | Adapter 13 | } from '../shared/asyncServices/http'; 14 | import { AuthSandbox } from './auth.sandbox'; 15 | 16 | @Injectable() 17 | @DefaultHeaders({ 18 | 'Accept': 'application/json', 19 | 'Content-Type': 'application/json' 20 | }) 21 | export class AuthApiClient extends HttpService { 22 | 23 | /** 24 | * Submits login form to the server 25 | * 26 | * @param form 27 | */ 28 | @POST("/account/login") 29 | @Adapter(AuthSandbox.authAdapter) 30 | public login(@Body form: LoginForm): Observable { return null; }; 31 | 32 | /** 33 | * Submits register form to the server 34 | * 35 | * @param form 36 | */ 37 | @POST("/account/register") 38 | @Adapter(AuthSandbox.authAdapter) 39 | public register(@Body form: RegisterForm): Observable { return null; }; 40 | 41 | /** 42 | * Logs out current user 43 | */ 44 | @POST("/account/logout") 45 | public logout(): Observable { return null; }; 46 | } -------------------------------------------------------------------------------- /src/app/auth/login/login.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | 7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
-------------------------------------------------------------------------------- /src/app/shared/components/pageNotFound/pageNotFound.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .pageNotFound { 4 | position: fixed; 5 | top: 0; 6 | right: 0; 7 | bottom: 0; 8 | left: 0; 9 | background-color: $color-theme-violet; 10 | background: $color-theme-violet url(/assets/images/stars.png) center center; 11 | background-size: 100%; 12 | 13 | .pageNotFound-content { 14 | position: absolute; 15 | width: 550px; 16 | text-align: center; 17 | left: 50%; 18 | margin-left: -275px; 19 | top: 50%; 20 | margin-top: -230px; 21 | } 22 | 23 | img { 24 | width: 22%; 25 | } 26 | 27 | h1 { 28 | color: $color-white; 29 | font-weight: 100; 30 | font-size: 45px; 31 | letter-spacing: 1px; 32 | margin-bottom: 10px; 33 | } 34 | 35 | h3 { 36 | color: $color-white; 37 | font-weight: 100; 38 | font-size: 22px; 39 | margin: 0; 40 | } 41 | 42 | button { 43 | text-transform: uppercase; 44 | background: none; 45 | color: $color-white; 46 | border: 1px solid $color-white; 47 | border-radius: 5px; 48 | padding: 8px 30px; 49 | margin-top: 30px; 50 | outline: none; 51 | 52 | &:hover { 53 | background-color: $color-theme-darkviolet; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/app/shared/components/spinner/spinner.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .spinner { 4 | position: absolute; 5 | top: 0; 6 | right: 0; 7 | bottom: 0; 8 | left: 0; 9 | border-radius: 8px; 10 | } 11 | 12 | .spinner-inner-wrapper { 13 | width: 40px; 14 | height: 40px; 15 | position: absolute; 16 | top: 50%; 17 | left: 50%; 18 | margin: -20px; 19 | 20 | &.spinner-small { 21 | width: 20px; 22 | height: 20px; 23 | margin: -10px; 24 | } 25 | } 26 | 27 | .double-bounce1, .double-bounce2 { 28 | width: 100%; 29 | height: 100%; 30 | border-radius: 50%; 31 | background-color: #46a9d4; 32 | opacity: 0.7; 33 | position: absolute; 34 | top: 0; 35 | left: 0; 36 | 37 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 38 | animation: sk-bounce 2.0s infinite ease-in-out; 39 | } 40 | 41 | .double-bounce2 { 42 | -webkit-animation-delay: -1.0s; 43 | animation-delay: -1.0s; 44 | } 45 | 46 | @-webkit-keyframes sk-bounce { 47 | 0%, 100% { -webkit-transform: scale(0.0) } 48 | 50% { -webkit-transform: scale(1.0) } 49 | } 50 | 51 | @keyframes sk-bounce { 52 | 0%, 100% { 53 | transform: scale(0.0); 54 | -webkit-transform: scale(0.0); 55 | } 50% { 56 | transform: scale(1.0); 57 | -webkit-transform: scale(1.0); 58 | } 59 | } -------------------------------------------------------------------------------- /src/app/shared/store/reducers/settings.reducer.ts: -------------------------------------------------------------------------------- 1 | import '@ngrx/core/add/operator/select'; 2 | import 'rxjs/add/operator/map'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import * as settings from '../actions/settings.action'; 5 | 6 | export interface State { 7 | selectedLanguage: string; 8 | selectedCulture: string; 9 | availableLanguages: Array 10 | }; 11 | 12 | const INITIAL_STATE: State = { 13 | selectedLanguage: '', 14 | selectedCulture: '', 15 | availableLanguages: [ 16 | {code: 'hr', name: 'HR', culture: 'hr-HR'}, 17 | {code: 'en', name: 'EN', culture: 'en-EN'} 18 | ] 19 | }; 20 | 21 | export function reducer(state = INITIAL_STATE, action: settings.Actions): State { 22 | switch (action.type) { 23 | case settings.ActionTypes.SET_LANGUAGE: { 24 | return Object.assign({}, state, { selectedLanguage: action.payload }); 25 | } 26 | 27 | case settings.ActionTypes.SET_CULTURE: { 28 | return Object.assign({}, state, { selectedCulture: action.payload }); 29 | } 30 | 31 | default: { 32 | return state; 33 | } 34 | } 35 | } 36 | 37 | export const getSelectedLanguage = (state: State) => state.selectedLanguage; 38 | export const getSelectedCulture = (state: State) => state.selectedCulture; 39 | export const getAvailableLanguages = (state: State) => state.availableLanguages; -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { 5 | FormsModule, 6 | ReactiveFormsModule 7 | } from '@angular/forms'; 8 | import { AuthRoutingModule } from './auth-routing.module'; 9 | import { RegisterComponent } from './register/register.component'; 10 | import { LoginComponent } from './login/login.component'; 11 | import { ComponentsModule } from '../shared/components'; 12 | import { TranslateModule } from 'ng2-translate'; 13 | import { SimpleNotificationsModule } from 'angular2-notifications'; 14 | import { AuthSandbox } from './auth.sandbox'; 15 | import { AuthApiClient } from './authApiClient.service'; 16 | 17 | @NgModule({ 18 | imports: [ 19 | CommonModule, 20 | BrowserAnimationsModule, 21 | AuthRoutingModule, 22 | FormsModule, 23 | ReactiveFormsModule, 24 | ComponentsModule, 25 | TranslateModule, 26 | SimpleNotificationsModule 27 | ], 28 | declarations: [ 29 | RegisterComponent, 30 | LoginComponent 31 | ], 32 | providers: [ 33 | AuthApiClient, 34 | AuthSandbox 35 | ] 36 | }) 37 | export class AuthModule {} 38 | -------------------------------------------------------------------------------- /src/app/shared/components/spinner/spinner.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'spinner', 5 | template: ` 6 |
7 |
8 |
9 |
10 |
11 |
12 | `, 13 | styleUrls: ['./spinner.component.scss'] 14 | }) 15 | export class SpinnerComponent { 16 | // private currentTimeout: any; 17 | // private isDelayedRunning: boolean = false; 18 | 19 | // @Input() 20 | // public delay: number = 300; 21 | 22 | @Input() isRunning: boolean; 23 | @Input() isSmall: string; 24 | // public set isRunning(value: boolean) { 25 | // if (!value) { 26 | // this.cancelTimeout(); 27 | // this.isDelayedRunning = false; 28 | // return; 29 | // } 30 | 31 | // if (this.currentTimeout) { 32 | // return; 33 | // } 34 | 35 | // this.currentTimeout = setTimeout(() => { 36 | // this.isDelayedRunning = value; 37 | // this.cancelTimeout(); 38 | // }, this.delay); 39 | // } 40 | 41 | // private cancelTimeout(): void { 42 | // clearTimeout(this.currentTimeout); 43 | // this.currentTimeout = undefined; 44 | // } 45 | 46 | // ngOnDestroy(): any { 47 | // this.cancelTimeout(); 48 | // } 49 | } -------------------------------------------------------------------------------- /src/app/shared/utility/validation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | FormControl, 4 | FormGroup 5 | } from '@angular/forms'; 6 | @Injectable() 7 | export class ValidationService { 8 | 9 | /** 10 | * Validates email address 11 | * 12 | * @param formControl 13 | */ 14 | public validateEmail(formControl: FormControl): {[error: string]: any} { 15 | let EMAIL_REGEXP = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 16 | return EMAIL_REGEXP.test(formControl.value) ? null : { validateEmail: { valid: false } }; 17 | } 18 | 19 | /** 20 | * Validates required numeric values 21 | * 22 | * @param formControl 23 | */ 24 | public numericRequired(formControl: FormControl): {[error: string]: any} { 25 | return (formControl.value && formControl.value > 0) ? null : { numericRequired: { valid: false } }; 26 | } 27 | 28 | /** 29 | * Validates matching string values 30 | * 31 | * @param controlKey 32 | * @param matchingControlKey 33 | */ 34 | public matchingPasswords(controlKey: string, matchingControlKey: string): {[error: string]: any} { 35 | return (group: FormGroup): {[key: string]: any} => { 36 | if (group.controls[controlKey].value !== group.controls[matchingControlKey].value) { 37 | return { mismatch: { valid: false } }; 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/app/shared/components/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Output, 4 | Input, 5 | EventEmitter, 6 | ChangeDetectionStrategy 7 | } from '@angular/core'; 8 | 9 | @Component({ 10 | selector: 'app-header', 11 | template: ` 12 |
13 | 16 | 17 |
18 | 22 | 23 |
24 |
25 | 29 | 30 |
31 |
32 | `, 33 | styleUrls: ['./header.component.scss'], 34 | changeDetection: ChangeDetectionStrategy.OnPush 35 | }) 36 | export class HeaderComponent { 37 | @Input() selectedLanguage: string; 38 | @Input() availableLanguages: Array; 39 | @Input() userImage: string; 40 | @Input() userEmail: string; 41 | 42 | @Output() selectLanguage: EventEmitter = new EventEmitter(); 43 | @Output() logout: EventEmitter = new EventEmitter(); 44 | } 45 | -------------------------------------------------------------------------------- /src/styles/paddings.scss: -------------------------------------------------------------------------------- 1 | .wrapper-xs { 2 | padding: 5px!important; 3 | } 4 | 5 | .wrapper-sm { 6 | padding: 10px!important; 7 | } 8 | 9 | .wrapper { 10 | padding: 15px!important; 11 | } 12 | 13 | .wrapper-md { 14 | padding: 20px!important; 15 | } 16 | 17 | .wrapper-lg { 18 | padding: 30px!important; 19 | } 20 | 21 | .wrapper-xl { 22 | padding: 50px!important; 23 | } 24 | 25 | .padder-lg { 26 | padding-left: 30px!important; 27 | padding-right: 30px!important; 28 | } 29 | 30 | .padder-md { 31 | padding-left: 20px!important; 32 | padding-right: 20px!important; 33 | } 34 | 35 | .padder { 36 | padding-left: 15px!important; 37 | padding-right: 15px!important; 38 | } 39 | 40 | .padder-sm { 41 | padding-left: 10px!important; 42 | padding-right: 10px!important; 43 | } 44 | 45 | .padder-v { 46 | padding-top: 15px!important; 47 | padding-bottom: 15px!important; 48 | } 49 | 50 | .padder-v-sm { 51 | padding-top: 10px!important; 52 | padding-bottom: 10px!important; 53 | } 54 | 55 | .padder-v-xs { 56 | padding-top: 5px!important; 57 | padding-bottom: 5px!important; 58 | } 59 | 60 | .padder-r-md { 61 | padding-right: 25px!important; 62 | } 63 | 64 | .padder-l-xs { 65 | padding-left: 5px!important; 66 | } 67 | 68 | .padder-l-sm { 69 | padding-left: 10px!important; 70 | } 71 | 72 | .padder-l-md { 73 | padding-left: 25px!important; 74 | } 75 | 76 | .padder-b-md { 77 | padding-bottom: 25px!important; 78 | } 79 | 80 | .no-padder { 81 | padding: 0 !important; 82 | } -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | FormsModule, 4 | ReactiveFormsModule 5 | } from '@angular/forms'; 6 | import { RouterModule } from '@angular/router'; 7 | import { CommonModule } from '@angular/common'; 8 | 9 | import { PipesModule } from '../pipes'; 10 | import { TranslateModule } from 'ng2-translate'; 11 | 12 | import { SpinnerComponent } from './spinner/spinner.component'; 13 | import { NavigationComponent } from './navigation/navigation.component'; 14 | import { ProfileActionBarComponent } from './profileActionBar/profileActionBar.component'; 15 | import { HeaderComponent } from './header/header.component'; 16 | import { LanguageSelectorComponent } from './languageSelector/languageSelector.component'; 17 | import { PageNotFoundComponent } from './pageNotFound/pageNotFound.component'; 18 | 19 | export const COMPONENTS = [ 20 | SpinnerComponent, 21 | NavigationComponent, 22 | ProfileActionBarComponent, 23 | HeaderComponent, 24 | LanguageSelectorComponent, 25 | PageNotFoundComponent 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [ 30 | RouterModule, 31 | FormsModule, 32 | ReactiveFormsModule, 33 | CommonModule, 34 | TranslateModule, 35 | PipesModule 36 | ], 37 | declarations: COMPONENTS, 38 | exports: COMPONENTS 39 | }) 40 | export class ComponentsModule { } -------------------------------------------------------------------------------- /src/app/shared/store/reducers/products.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions/products.action'; 2 | import { Product } from '../../models'; 3 | 4 | export interface State { 5 | loading: boolean; 6 | loaded: boolean; 7 | failed: boolean; 8 | data: Array; 9 | }; 10 | 11 | const INITIAL_STATE: State = { 12 | loading: false, 13 | loaded: false, 14 | failed: false, 15 | data: [] 16 | }; 17 | 18 | export function reducer(state = INITIAL_STATE, action: actions.Actions): State { 19 | if (!action) return state; 20 | 21 | switch (action.type) { 22 | case actions.ActionTypes.LOAD: { 23 | return Object.assign({}, state, { 24 | loading: true 25 | }); 26 | } 27 | 28 | case actions.ActionTypes.LOAD_SUCCESS: { 29 | return Object.assign({}, state, { 30 | loaded: true, 31 | loading: false, 32 | failed: false, 33 | data: action.payload 34 | }); 35 | } 36 | 37 | case actions.ActionTypes.LOAD_FAIL: { 38 | return Object.assign({}, state, { 39 | loaded: false, 40 | loading: false, 41 | failed: true, 42 | data: [] 43 | }); 44 | } 45 | 46 | default: { 47 | return state; 48 | } 49 | } 50 | }; 51 | 52 | export const getData = (state: State) => state.data; 53 | export const getLoading = (state: State) => state.loading; 54 | export const getLoaded = (state: State) => state.loaded; 55 | export const getFailed = (state: State) => state.failed; -------------------------------------------------------------------------------- /src/app/shared/store/reducers/product-details.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions/product-details.action'; 2 | import { Product } from '../../models'; 3 | 4 | export interface State { 5 | loading: boolean; 6 | loaded: boolean; 7 | failed: boolean; 8 | data: Product; 9 | }; 10 | 11 | const INITIAL_STATE: State = { 12 | loading: false, 13 | loaded: false, 14 | failed: false, 15 | data: null 16 | }; 17 | 18 | export function reducer(state = INITIAL_STATE, action: actions.Actions): State { 19 | if (!action) return state; 20 | 21 | switch (action.type) { 22 | case actions.ActionTypes.LOAD: { 23 | return Object.assign({}, state, { 24 | loading: true 25 | }); 26 | } 27 | 28 | case actions.ActionTypes.LOAD_SUCCESS: { 29 | return Object.assign({}, state, { 30 | loaded: true, 31 | loading: false, 32 | failed: false, 33 | data: action.payload 34 | }); 35 | } 36 | 37 | case actions.ActionTypes.LOAD_FAIL: { 38 | return Object.assign({}, state, { 39 | loaded: false, 40 | loading: false, 41 | failed: true, 42 | data: null 43 | }); 44 | } 45 | 46 | default: { 47 | return state; 48 | } 49 | } 50 | }; 51 | 52 | export const getData = (state: State) => state.data; 53 | export const getLoading = (state: State) => state.loading; 54 | export const getLoaded = (state: State) => state.loaded; 55 | export const getFailed = (state: State) => state.failed; -------------------------------------------------------------------------------- /src/app/shared/components/languageSelector/languageSelector.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Output, 4 | Input, 5 | EventEmitter, 6 | ChangeDetectionStrategy, 7 | ElementRef 8 | } from '@angular/core'; 9 | 10 | @Component({ 11 | selector: 'language-selector', 12 | template: ` 13 | 14 | 15 | 18 | `, 19 | styleUrls: ['./languageSelector.component.scss'], 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | host: { 22 | '(document:click)': 'onDocumentClick($event)' 23 | } 24 | }) 25 | export class LanguageSelectorComponent { 26 | 27 | @Input() selectedLanguage: string; 28 | @Input() availableLanguages: Array; 29 | @Output() select: EventEmitter = new EventEmitter(); 30 | 31 | public show: boolean = false; 32 | 33 | constructor(private elementRef: ElementRef) {} 34 | 35 | public onDocumentClick(event): void { 36 | if (!this.elementRef.nativeElement.contains(event.target)) { 37 | this.show = false; 38 | } 39 | } 40 | 41 | public onToggle(): void { 42 | this.show = !this.show; 43 | } 44 | 45 | public selectLanguage(lang: any) { 46 | this.show = false; 47 | this.select.emit({code: lang.code, culture: lang.culture}); 48 | } 49 | } -------------------------------------------------------------------------------- /src/app/shared/components/loadingPlaceholder/loadingPlaceholder.component.ts: -------------------------------------------------------------------------------- 1 | // import { Component, Output, Input, EventEmitter, ChangeDetectionStrategy, ElementRef } from '@angular/core'; 2 | 3 | // @Component({ 4 | // selector: 'loading-placeholder', 5 | // template: ` 6 | //
7 | //
8 | //
9 | //
10 | //
11 | //
12 | //
13 | //
14 | //
15 | //
16 | //
17 | //
18 | //
19 | //
20 | //
21 | //
22 | //
23 | // `, 24 | // styleUrls: ['./loadingPlaceholder.component.scss'], 25 | // changeDetection: ChangeDetectionStrategy.OnPush 26 | // }) 27 | // export class LoadingPlaceholderComponent { 28 | 29 | // @Input() isRunning: boolean; 30 | 31 | // constructor() {} 32 | // } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/css/fontello-codes.css: -------------------------------------------------------------------------------- 1 | 2 | .icon-mail:before { content: '\e800'; } /* '' */ 3 | .icon-user:before { content: '\e801'; } /* '' */ 4 | .icon-th-large:before { content: '\e802'; } /* '' */ 5 | .icon-th-list:before { content: '\e803'; } /* '' */ 6 | .icon-ok:before { content: '\e804'; } /* '' */ 7 | .icon-eye:before { content: '\e805'; } /* '' */ 8 | .icon-bell:before { content: '\e806'; } /* '' */ 9 | .icon-cog:before { content: '\e807'; } /* '' */ 10 | .icon-wrench:before { content: '\e808'; } /* '' */ 11 | .icon-calendar:before { content: '\e809'; } /* '' */ 12 | .icon-logout:before { content: '\e80a'; } /* '' */ 13 | .icon-down-open:before { content: '\e80b'; } /* '' */ 14 | .icon-up-open:before { content: '\e80c'; } /* '' */ 15 | .icon-right-open:before { content: '\e80d'; } /* '' */ 16 | .icon-left-open:before { content: '\e80e'; } /* '' */ 17 | .icon-comment:before { content: '\e80f'; } /* '' */ 18 | .icon-chart-bar:before { content: '\e810'; } /* '' */ 19 | .icon-box:before { content: '\e811'; } /* '' */ 20 | .icon-mail-alt:before { content: '\f0e0'; } /* '' */ 21 | .icon-comment-empty:before { content: '\f0e5'; } /* '' */ 22 | .icon-bell-alt:before { content: '\f0f3'; } /* '' */ 23 | .icon-angle-left:before { content: '\f104'; } /* '' */ 24 | .icon-angle-right:before { content: '\f105'; } /* '' */ 25 | .icon-angle-up:before { content: '\f106'; } /* '' */ 26 | .icon-angle-down:before { content: '\f107'; } /* '' */ 27 | .icon-ellipsis:before { content: '\f141'; } /* '' */ 28 | .icon-user-circle-o:before { content: '\f2be'; } /* '' */ 29 | .icon-user-o:before { content: '\f2c0'; } /* '' */ -------------------------------------------------------------------------------- /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-architecture-patterns" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico", 13 | "service-worker.js", 14 | { "glob": "**/*", "input": "../config", "output": "./config/" }, 15 | { "glob": "en.json", "input": "../i18n", "output": "./i18n/" }, 16 | { "glob": "hr.json", "input": "../i18n", "output": "./i18n/" } 17 | ], 18 | "index": "index.html", 19 | "main": "main.ts", 20 | "polyfills": "polyfills.ts", 21 | "test": "test.ts", 22 | "tsconfig": "tsconfig.app.json", 23 | "testTsconfig": "tsconfig.spec.json", 24 | "prefix": "app", 25 | "styles": [ 26 | "styles.scss" 27 | ], 28 | "scripts": [], 29 | "environmentSource": "environments/environment.ts", 30 | "environments": { 31 | "dev": "environments/environment.ts", 32 | "prod": "environments/environment.prod.ts" 33 | } 34 | } 35 | ], 36 | "e2e": { 37 | "protractor": { 38 | "config": "./protractor.conf.js" 39 | } 40 | }, 41 | "lint": [ 42 | { 43 | "project": "src/tsconfig.app.json" 44 | }, 45 | { 46 | "project": "src/tsconfig.spec.json" 47 | }, 48 | { 49 | "project": "e2e/tsconfig.e2e.json" 50 | } 51 | ], 52 | "test": { 53 | "karma": { 54 | "config": "./karma.conf.js" 55 | } 56 | }, 57 | "defaults": { 58 | "styleExt": "css", 59 | "component": {} 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/auth/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ 'Auth.Register.Back' | translate }} 3 | 4 |

{{ 'Auth.Register.Title' | translate }}

5 | 6 |
7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
-------------------------------------------------------------------------------- /src/app/shared/components/profileActionBar/profileActionBar.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .profileActionBar { 4 | height: 100%; 5 | } 6 | 7 | .profileActionBar-anchor { 8 | min-width: 200px; 9 | padding: 10px; 10 | height: 100%; 11 | 12 | span, 13 | i { 14 | display: inline-block; 15 | color: $color-grey; 16 | } 17 | } 18 | 19 | .profileActionBar-logout { 20 | cursor: pointer; 21 | 22 | &:hover { 23 | text-decoration: underline; 24 | } 25 | } 26 | 27 | .profileActionBar-imgWrapper { 28 | display: inline-block; 29 | vertical-align: middle; 30 | width: 50px; 31 | height: 50px; 32 | border: 1px solid $color-grey-verylight; 33 | border-radius: 50%; 34 | overflow: hidden; 35 | margin-right: 5px; 36 | cursor: default; 37 | 38 | img { 39 | width: 100% !important; 40 | height: auto !important; 41 | } 42 | } 43 | 44 | .profileActionBar-menu { 45 | background-color: $color-white; 46 | width: 203px; 47 | -webkit-box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.24); 48 | -moz-box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.24); 49 | box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.24); 50 | 51 | .k-popup { 52 | border: none; 53 | } 54 | } 55 | 56 | .profileActionBar-menu { 57 | border-radius: 0; 58 | 59 | * { 60 | color: $color-grey; 61 | } 62 | } 63 | 64 | .profileActionBar-list { 65 | width: 100%; 66 | 67 | &:hover { 68 | background-color: $color-theme-lightBlue; 69 | } 70 | } 71 | 72 | .profileActionBar-item { 73 | padding: 10px 0px; 74 | width: 100%; 75 | cursor: pointer; 76 | } 77 | 78 | .profileActionBar-link { 79 | padding: 0 10px; 80 | 81 | i { 82 | margin-right: 10px; 83 | } 84 | } -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular/cli'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false }, 20 | { pattern: 'config/*.json', watched: false, included: false, served: true, nocache: false }, 21 | { pattern: 'i18n/*.json', watched: false, included: false, served: true, nocache: false } 22 | ], 23 | proxies: { 24 | '/config/': '/base/config/', 25 | '/i18n/': '/base/i18n/' 26 | }, 27 | preprocessors: { 28 | './src/test.ts': ['@angular/cli'] 29 | }, 30 | mime: { 31 | 'text/x-typescript': ['ts','tsx'] 32 | }, 33 | coverageIstanbulReporter: { 34 | reports: [ 'html', 'lcovonly' ], 35 | fixWebpackSourcePaths: true 36 | }, 37 | angularCli: { 38 | environment: 'dev' 39 | }, 40 | reporters: config.angularCli && config.angularCli.codeCoverage 41 | ? ['progress', 'coverage-istanbul'] 42 | : ['progress', 'kjhtml'], 43 | port: 9876, 44 | colors: true, 45 | logLevel: config.LOG_INFO, 46 | autoWatch: true, 47 | browsers: ['Chrome'], 48 | singleRun: false 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { BrowserModule } from "@angular/platform-browser"; 4 | import { 5 | ReactiveFormsModule, 6 | NG_VALIDATORS, 7 | FormControl 8 | } from "@angular/forms"; 9 | import { RouterModule } from '@angular/router'; 10 | import { ProductsRoutingModule } from './products-routing.module'; 11 | import { ProductsComponent } from './products.component'; 12 | import { ProductDetailsComponent } from './product-details.component'; 13 | import { ProductsSandbox } from './products.sandbox'; 14 | import { ProductsApiClient } from './productsApiClient.service'; 15 | import { ProductsService } from './products.service'; 16 | import { ProductsResolver } from './products.resolver'; 17 | 18 | import { ComponentsModule } from '../shared/components'; 19 | import { ContainersModule } from '../shared/containers'; 20 | import { TranslateModule } from 'ng2-translate'; 21 | import { NgxDatatableModule } from '@swimlane/ngx-datatable'; 22 | 23 | @NgModule({ 24 | imports: [ 25 | CommonModule, 26 | ProductsRoutingModule, 27 | ComponentsModule, 28 | ContainersModule, 29 | TranslateModule, 30 | BrowserModule, 31 | ReactiveFormsModule, 32 | RouterModule, 33 | NgxDatatableModule 34 | ], 35 | declarations: [ 36 | ProductsComponent, 37 | ProductDetailsComponent 38 | ], 39 | providers: [ 40 | ProductsSandbox, 41 | ProductsService, 42 | ProductsApiClient, 43 | ProductsResolver 44 | ] 45 | }) 46 | export class ProductsModule {} 47 | -------------------------------------------------------------------------------- /src/app/shared/containers/layout/layout.sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Sandbox } from '../../sandbox/base.sandbox'; 4 | import { Store } from '@ngrx/store'; 5 | import * as store from '../../store'; 6 | import * as authActions from '../../store/actions/auth.action'; 7 | import * as settingsActions from '../../store/actions/settings.action'; 8 | import { TranslateService } from 'ng2-translate'; 9 | 10 | @Injectable() 11 | export class LayoutSandbox extends Sandbox { 12 | 13 | public selectedLang$ = this.appState$.select(store.getSelectedLanguage); 14 | public availableLanguages$ = this.appState$.select(store.getAvailableLanguages); 15 | public user$ = this.appState$.select(store.getLoggedUser); 16 | private loginLoaded$; 17 | 18 | constructor( 19 | protected appState$: Store, 20 | private translateService: TranslateService, 21 | private router: Router 22 | ) { 23 | super(appState$); 24 | } 25 | 26 | public selectLanguage(lang: any): void { 27 | this.appState$.dispatch(new settingsActions.SetLanguageAction(lang.code)); 28 | this.appState$.dispatch(new settingsActions.SetCultureAction(lang.culture)); 29 | this.translateService.use(lang.code); 30 | } 31 | 32 | public logout() { 33 | this.appState$.dispatch(new authActions.DoLogoutAction()); 34 | this.subscribeToLoginChanges(); 35 | } 36 | 37 | private subscribeToLoginChanges() { 38 | if (this.loginLoaded$) return; 39 | 40 | this.loginLoaded$ = this.appState$.select(store.getAuthLoaded) 41 | .subscribe(loaded => { 42 | if (!loaded) this.router.navigate(['/login']) 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /src/app/app-config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { 4 | Http, 5 | Headers, 6 | RequestOptions 7 | } from '@angular/http'; 8 | 9 | @Injectable() 10 | export class ConfigService { 11 | 12 | private config: Object 13 | private env: Object 14 | 15 | constructor(private http: Http) {} 16 | 17 | /** 18 | * Loads the environment config file first. Reads the environment variable from the file 19 | * and based on that loads the appropriate configuration file - development or production 20 | */ 21 | load() { 22 | return new Promise((resolve, reject) => { 23 | let headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/json', 'DataType': 'application/json' }); 24 | let options = new RequestOptions({ headers: headers }); 25 | 26 | this.http.get('/config/env.json') 27 | .map(res => res.json()) 28 | .subscribe((env_data) => { 29 | this.env = env_data; 30 | 31 | this.http.get('/config/' + env_data.env + '.json') 32 | .map(res => res.json()) 33 | .catch((error: any) => { 34 | return Observable.throw(error.json().error || 'Server error'); 35 | }) 36 | .subscribe((data) => { 37 | this.config = data; 38 | resolve(true); 39 | }); 40 | }); 41 | 42 | }); 43 | } 44 | 45 | /** 46 | * Returns environment variable based on given key 47 | * 48 | * @param key 49 | */ 50 | getEnv(key: any) { 51 | return this.env[key]; 52 | } 53 | 54 | /** 55 | * Returns configuration value based on given key 56 | * 57 | * @param key 58 | */ 59 | get(key: any) { 60 | return this.config[key]; 61 | } 62 | } -------------------------------------------------------------------------------- /src/app/app.sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Sandbox } from './shared/sandbox/base.sandbox'; 3 | import { Store } from '@ngrx/store'; 4 | import * as store from './shared/store'; 5 | import * as settingsActions from './shared/store/actions/settings.action'; 6 | import { TranslateService } from 'ng2-translate'; 7 | import { ConfigService } from './app-config.service'; 8 | 9 | @Injectable() 10 | export class AppSandbox extends Sandbox { 11 | 12 | constructor( 13 | protected appState$: Store, 14 | private translate: TranslateService, 15 | private configService: ConfigService 16 | ) { 17 | super(appState$); 18 | } 19 | 20 | /** 21 | * Sets up default language for the application. Uses browser default language. 22 | */ 23 | public setupLanguage(): void { 24 | let localization: any = this.configService.get('localization'); 25 | let languages: Array = localization.languages.map(lang => lang.code); 26 | let browserLang: string = this.translate.getBrowserLang(); 27 | 28 | this.translate.addLangs(languages); 29 | this.translate.setDefaultLang(localization.defaultLanguage); 30 | 31 | let selectedLang = browserLang.match(/en|hr/) ? browserLang : localization.defaultLanguage; 32 | let selectedCulture = localization.languages.filter(lang => lang.code === selectedLang)[0].culture; 33 | 34 | this.translate.use(selectedLang); 35 | this.appState$.dispatch(new settingsActions.SetLanguageAction(selectedLang)); 36 | this.appState$.dispatch(new settingsActions.SetCultureAction(selectedCulture)); 37 | } 38 | 39 | /** 40 | * Returns global notification options 41 | */ 42 | public getNotificationOptions(): any { 43 | return this.configService.get('notifications').options; 44 | } 45 | } -------------------------------------------------------------------------------- /src/app/shared/containers/layout/layout.container.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Observable } from 'rxjs/Observable'; 3 | import { Subscription } from "rxjs"; 4 | import { LayoutSandbox } from './layout.sandbox'; 5 | import { ConfigService } from '../../../app-config.service'; 6 | 7 | @Component({ 8 | selector: 'app-layout', 9 | styleUrls: ['./layout.container.scss'], 10 | template: ` 11 | 18 | 19 | 20 |
21 | 22 |
23 | ` 24 | }) 25 | export class LayoutContainer { 26 | 27 | public userImage: string = ''; 28 | public userEmail: string = ''; 29 | private assetsFolder: string; 30 | 31 | private subscriptions: Array = []; 32 | 33 | constructor( 34 | private configService: ConfigService, 35 | public layoutSandbox: LayoutSandbox 36 | ) { 37 | this.assetsFolder = this.configService.get('paths').userImageFolder; 38 | } 39 | 40 | ngOnInit() { 41 | this.registerEvents(); 42 | } 43 | 44 | ngOnDestroy() { 45 | this.subscriptions.forEach(sub => sub.unsubscribe()); 46 | } 47 | 48 | private registerEvents() { 49 | // Subscribes to user changes 50 | this.subscriptions.push(this.layoutSandbox.user$.subscribe(user => { 51 | if (user) { 52 | this.userImage = this.assetsFolder + 'user.jpg'; 53 | this.userEmail = user.email; 54 | } 55 | })); 56 | } 57 | } -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/http.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { 3 | Http, 4 | Request, 5 | RequestMethod, 6 | Response 7 | } from "@angular/http"; 8 | import { Observable } from "rxjs/Observable"; 9 | import { HttpResponseHandler } from './httpResponseHandler.service'; 10 | import { HttpAdapter } from './http.adapter'; 11 | import { ConfigService } from '../../../app-config.service'; 12 | import { 13 | methodBuilder, 14 | paramBuilder 15 | } from './utils.service'; 16 | 17 | /** 18 | * Supported @Produces media types 19 | */ 20 | export enum MediaType { 21 | JSON, 22 | FORM_DATA 23 | } 24 | 25 | @Injectable() 26 | export class HttpService { 27 | 28 | public constructor( 29 | protected http: Http, 30 | protected configService: ConfigService, 31 | protected responseHandler: HttpResponseHandler) { 32 | } 33 | 34 | protected getBaseUrl(): string { 35 | return this.configService.get('api').baseUrl; 36 | } 37 | 38 | protected getDefaultHeaders(): Object { 39 | return null; 40 | } 41 | 42 | /** 43 | * Request Interceptor 44 | * 45 | * @method requestInterceptor 46 | * @param {Request} req - request object 47 | */ 48 | protected requestInterceptor(req: Request) {} 49 | 50 | /** 51 | * Response Interceptor 52 | * 53 | * @method responseInterceptor 54 | * @param {Response} observableRes - response object 55 | * @returns {Response} res - transformed response object 56 | */ 57 | protected responseInterceptor(observableRes: Observable, adapterFn?: Function): Observable { 58 | return observableRes 59 | .map(res => HttpAdapter.baseAdapter(res, adapterFn)) 60 | .catch((err, source) => this.responseHandler.onCatch(err, source)); 61 | } 62 | } -------------------------------------------------------------------------------- /src/app/auth/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy 4 | } from '@angular/core'; 5 | import { 6 | FormBuilder, 7 | FormGroup, 8 | Validators, 9 | AbstractControl 10 | } from '@angular/forms'; 11 | import { moveIn } from '../../shared/animations'; 12 | import { AuthSandbox } from '../auth.sandbox'; 13 | 14 | @Component({ 15 | selector: 'login', 16 | templateUrl: './login.component.html', 17 | styleUrls: ['./login.component.scss'], 18 | animations: [moveIn()], 19 | host: {'[@moveIn]': ''}, 20 | changeDetection: ChangeDetectionStrategy.OnPush 21 | }) 22 | export class LoginComponent { 23 | 24 | public submitted: boolean = false; 25 | public email: AbstractControl; 26 | public password: AbstractControl; 27 | public loginForm: FormGroup; 28 | 29 | constructor( 30 | private fb: FormBuilder, 31 | public authSandbox: AuthSandbox 32 | ) {} 33 | 34 | ngOnInit() { 35 | this.initLoginForm(); 36 | } 37 | 38 | /** 39 | * Builds a form instance (using FormBuilder) with corresponding validation rules 40 | */ 41 | public initLoginForm(): void { 42 | this.loginForm = this.fb.group({ 43 | email: ['', [Validators.required, this.authSandbox.validationService.validateEmail]], 44 | password: ['', Validators.required] 45 | }); 46 | 47 | this.email = this.loginForm.controls['email']; 48 | this.password = this.loginForm.controls['password']; 49 | } 50 | 51 | /** 52 | * Handles form 'submit' event. Calls sandbox login function if form is valid. 53 | * 54 | * @param event 55 | * @param form 56 | */ 57 | public onSubmit(event: Event, form: any): void { 58 | event.stopPropagation(); 59 | this.submitted = true; 60 | 61 | if (this.loginForm.valid) this.authSandbox.login(form); 62 | } 63 | } -------------------------------------------------------------------------------- /src/app/shared/store/reducers/auth.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions/auth.action'; 2 | import { User } from '../../models'; 3 | 4 | export interface State { 5 | loading: boolean; 6 | loaded: boolean; 7 | failed: boolean; 8 | user: User; 9 | }; 10 | 11 | const INITIAL_STATE: State = { 12 | loading: false, 13 | loaded: false, 14 | failed: false, 15 | user: new User() 16 | }; 17 | 18 | export function reducer(state = INITIAL_STATE, action: actions.Actions): State { 19 | if (!action) return state; 20 | 21 | switch (action.type) { 22 | case actions.ActionTypes.DO_LOGIN: 23 | case actions.ActionTypes.DO_REGISTER: 24 | case actions.ActionTypes.DO_LOGOUT: { 25 | return Object.assign({}, state, { 26 | loading: true, 27 | loaded: false, 28 | failed: false 29 | }); 30 | } 31 | 32 | case actions.ActionTypes.DO_LOGIN_SUCCESS: 33 | case actions.ActionTypes.DO_REGISTER_SUCCESS: { 34 | return Object.assign({}, state, { 35 | loaded: true, 36 | loading: false, 37 | failed: false, 38 | user: action.payload 39 | }); 40 | } 41 | 42 | case actions.ActionTypes.DO_LOGOUT_SUCCESS: { 43 | return Object.assign({}, state, INITIAL_STATE); 44 | } 45 | 46 | case actions.ActionTypes.DO_LOGIN_FAIL: 47 | case actions.ActionTypes.DO_REGISTER_FAIL: 48 | case actions.ActionTypes.DO_LOGOUT_FAIL: { 49 | return Object.assign({}, INITIAL_STATE, { failed: true }); 50 | } 51 | 52 | case actions.ActionTypes.ADD_USER: { 53 | return Object.assign({}, state, { user: action.payload }); 54 | } 55 | 56 | default: { 57 | return state; 58 | } 59 | } 60 | } 61 | 62 | export const getLoggedUser = (state: State) => state.user; 63 | export const getLoading = (state: State) => state.loading; 64 | export const getLoaded = (state: State) => state.loaded; 65 | export const getFailed = (state: State) => state.failed; -------------------------------------------------------------------------------- /src/app/shared/components/navigation/navigation.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/variables"; 2 | 3 | .sidebar { 4 | overflow: hidden; 5 | height: 100vh; 6 | width: 68px; 7 | position: fixed; 8 | z-index: 1000; 9 | left: 0; 10 | top: 70px; 11 | background-color: $color-theme-darkviolet; 12 | transition-duration: 0.3s; 13 | transition: width 0.3s; 14 | -moz-transition: width 0.3s; 15 | -webkit-transition: width 0.3s; 16 | -o-transition: width 0.3s; 17 | } 18 | .sidebar:hover { 19 | width: 240px; 20 | } 21 | .sidebar:hover .sidebar-menu a span { 22 | display: block; 23 | } 24 | .sidebar-content { 25 | white-space: nowrap; 26 | } 27 | .sidebar-menu { 28 | background-color: $color-theme-violet; 29 | padding-top: 70px; 30 | } 31 | .sidebar-menu a { 32 | display: block; 33 | position: relative; 34 | color: #ccc; 35 | padding: 13px 10px 13px 19px; 36 | transition: opacity 0.2s ease; 37 | font-size: 18px; 38 | font-weight: 400; 39 | color: #fff; 40 | opacity: 0.5; 41 | height: 56px; 42 | } 43 | .sidebar-menu a span { 44 | display: none; 45 | margin-left: 48px; 46 | padding: 3px 0; 47 | } 48 | .sidebar-menu a:hover { 49 | text-decoration: none; 50 | opacity: 1; 51 | } 52 | .sidebar-menu a.current { 53 | background-color: $color-theme-darkviolet; 54 | -webkit-box-shadow: inset -2px 0px 8px -3px rgba(0, 0, 0, 0.75); 55 | -moz-box-shadow: inset -2px 0px 8px -3px rgba(0, 0, 0, 0.75); 56 | box-shadow: inset -2px 0px 8px -3px rgba(0, 0, 0, 0.75); 57 | opacity: 1; 58 | padding-left: 16px; 59 | } 60 | .sidebar-menu a i { 61 | font-size: 20px; 62 | padding-left:10px; 63 | display: inline-block; 64 | position: absolute; 65 | width: 30px; 66 | height: 30px; 67 | background:url(/assets/images/icons_sprite.svg) no-repeat; 68 | background-size:70px 870px; 69 | } 70 | .sidebar-menu a i.products { 71 | background-position: -21px -97px; 72 | } 73 | 74 | .sidebar-logo { 75 | display: block; 76 | width: 100%; 77 | text-align: center; 78 | position: absolute; 79 | bottom: 70px; 80 | outline: none; 81 | background: $color-grey-verylight; 82 | padding: 5px 0; 83 | border-right: 1px solid $color-grey-light; 84 | } -------------------------------------------------------------------------------- /src/app/shared/utility/utility.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService } from 'ng2-translate'; 3 | import { NotificationsService } from 'angular2-notifications'; 4 | import { ConfigService } from '../../app-config.service'; 5 | import { Observable } from 'rxjs/Rx'; 6 | 7 | @Injectable() 8 | export class UtilService { 9 | 10 | constructor( 11 | private translateService: TranslateService, 12 | private notificationService: NotificationsService, 13 | private configService: ConfigService 14 | ) {} 15 | 16 | /** 17 | * Translates given message code and title code and displays corresponding notification 18 | * 19 | * @param messageTranslationCode 20 | * @param type 21 | * @param titleTranslationCode 22 | */ 23 | public displayNotification(messageTranslationCode: string, type: string = 'info', titleTranslationCode?: string) { 24 | let message: string = this.translateService.instant(messageTranslationCode); 25 | let title: string = titleTranslationCode ? this.translateService.instant(titleTranslationCode) : null; 26 | 27 | switch (type) { 28 | case "error": 29 | title = this.translateService.instant('ErrorNotificationTitle'); 30 | break; 31 | 32 | case "success": 33 | title = this.translateService.instant('SuccessNotificationTitle'); 34 | break; 35 | 36 | case "alert": 37 | title = this.translateService.instant('WarningNotificationTitle'); 38 | break; 39 | 40 | default: 41 | title = this.translateService.instant('InfoNotificationTitle'); 42 | break; 43 | } 44 | 45 | this.notificationService[type](title, message, this.configService.get('notifications').options); 46 | } 47 | 48 | /** 49 | * Translates lookup names by looking into lookup code 50 | * 51 | * @param data 52 | */ 53 | public translateLookupData(data: Array): Array { 54 | // Translate quantity stock adjustment reasons 55 | return data.map(lookup => { 56 | lookup.name = lookup.code ? this.translateService.instant('Lookups')[lookup.code] : lookup.name; 57 | return lookup; 58 | }); 59 | } 60 | } -------------------------------------------------------------------------------- /src/app/products/products.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ChangeDetectionStrategy 4 | } from '@angular/core'; 5 | import { Router } from '@angular/router'; 6 | import { Subscription } from "rxjs"; 7 | import { ProductsSandbox } from './products.sandbox'; 8 | import { Product } from '../shared/models'; 9 | 10 | @Component({ 11 | selector: 'app-products', 12 | template: ` 13 | 14 |

{{ 'Products.Title' | translate }}

15 |
16 | 34 | 35 | 36 | 37 |
38 |
39 | `, 40 | styles: ['.products-grid-wrapper { position: relative; }'], 41 | changeDetection: ChangeDetectionStrategy.OnPush 42 | }) 43 | export class ProductsComponent { 44 | 45 | constructor( 46 | private router: Router, 47 | public productsSandbox: ProductsSandbox 48 | ) {} 49 | 50 | /** 51 | * Callback function for grid select event 52 | * 53 | * @param selected 54 | */ 55 | public onSelect({ selected }): void { 56 | this.productsSandbox.selectProduct(selected[0]); 57 | this.router.navigate(['/products', selected[0].id]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/css/animation.css: -------------------------------------------------------------------------------- 1 | /* 2 | Animation example, for spinners 3 | */ 4 | .animate-spin { 5 | -moz-animation: spin 2s infinite linear; 6 | -o-animation: spin 2s infinite linear; 7 | -webkit-animation: spin 2s infinite linear; 8 | animation: spin 2s infinite linear; 9 | display: inline-block; 10 | } 11 | @-moz-keyframes spin { 12 | 0% { 13 | -moz-transform: rotate(0deg); 14 | -o-transform: rotate(0deg); 15 | -webkit-transform: rotate(0deg); 16 | transform: rotate(0deg); 17 | } 18 | 19 | 100% { 20 | -moz-transform: rotate(359deg); 21 | -o-transform: rotate(359deg); 22 | -webkit-transform: rotate(359deg); 23 | transform: rotate(359deg); 24 | } 25 | } 26 | @-webkit-keyframes spin { 27 | 0% { 28 | -moz-transform: rotate(0deg); 29 | -o-transform: rotate(0deg); 30 | -webkit-transform: rotate(0deg); 31 | transform: rotate(0deg); 32 | } 33 | 34 | 100% { 35 | -moz-transform: rotate(359deg); 36 | -o-transform: rotate(359deg); 37 | -webkit-transform: rotate(359deg); 38 | transform: rotate(359deg); 39 | } 40 | } 41 | @-o-keyframes spin { 42 | 0% { 43 | -moz-transform: rotate(0deg); 44 | -o-transform: rotate(0deg); 45 | -webkit-transform: rotate(0deg); 46 | transform: rotate(0deg); 47 | } 48 | 49 | 100% { 50 | -moz-transform: rotate(359deg); 51 | -o-transform: rotate(359deg); 52 | -webkit-transform: rotate(359deg); 53 | transform: rotate(359deg); 54 | } 55 | } 56 | @-ms-keyframes spin { 57 | 0% { 58 | -moz-transform: rotate(0deg); 59 | -o-transform: rotate(0deg); 60 | -webkit-transform: rotate(0deg); 61 | transform: rotate(0deg); 62 | } 63 | 64 | 100% { 65 | -moz-transform: rotate(359deg); 66 | -o-transform: rotate(359deg); 67 | -webkit-transform: rotate(359deg); 68 | transform: rotate(359deg); 69 | } 70 | } 71 | @keyframes spin { 72 | 0% { 73 | -moz-transform: rotate(0deg); 74 | -o-transform: rotate(0deg); 75 | -webkit-transform: rotate(0deg); 76 | transform: rotate(0deg); 77 | } 78 | 79 | 100% { 80 | -moz-transform: rotate(359deg); 81 | -o-transform: rotate(359deg); 82 | -webkit-transform: rotate(359deg); 83 | transform: rotate(359deg); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/styles/icons.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: "data-table"; 5 | src:url("./assets/fonts/data-table.eot"); 6 | src:url("./assets/fonts/data-table.eot?#iefix") format("embedded-opentype"), 7 | url("./assets/fonts/data-table.woff") format("woff"), 8 | url("./assets/fonts/data-table.ttf") format("truetype"), 9 | url("./assets/fonts/data-table.svg#data-table") format("svg"); 10 | font-weight: normal; 11 | font-style: normal; 12 | } 13 | 14 | [data-icon]:before { 15 | font-family: "data-table" !important; 16 | content: attr(data-icon); 17 | font-style: normal !important; 18 | font-weight: normal !important; 19 | font-variant: normal !important; 20 | text-transform: none !important; 21 | speak: none; 22 | line-height: 1; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | [class^="datatable-icon-"]:before, 28 | [class*=" datatable-icon-"]:before { 29 | font-family: "data-table" !important; 30 | font-style: normal !important; 31 | font-weight: normal !important; 32 | font-variant: normal !important; 33 | text-transform: none !important; 34 | speak: none; 35 | line-height: 1; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-osx-font-smoothing: grayscale; 38 | } 39 | 40 | .datatable-icon-filter:before { 41 | content: "\62"; 42 | } 43 | .datatable-icon-collapse:before { 44 | content: "\61"; 45 | } 46 | .datatable-icon-expand:before { 47 | content: "\63"; 48 | } 49 | .datatable-icon-close:before { 50 | content: "\64"; 51 | } 52 | .datatable-icon-up:before { 53 | content: "\65"; 54 | } 55 | .datatable-icon-down:before { 56 | content: "\66"; 57 | } 58 | .datatable-icon-sort:before { 59 | content: "\67"; 60 | } 61 | .datatable-icon-done:before { 62 | content: "\68"; 63 | } 64 | .datatable-icon-done-all:before { 65 | content: "\69"; 66 | } 67 | .datatable-icon-search:before { 68 | content: "\6a"; 69 | } 70 | .datatable-icon-pin:before { 71 | content: "\6b"; 72 | } 73 | .datatable-icon-add:before { 74 | content: "\6d"; 75 | } 76 | .datatable-icon-left:before { 77 | content: "\6f"; 78 | } 79 | .datatable-icon-right:before { 80 | content: "\70"; 81 | } 82 | .datatable-icon-skip:before { 83 | content: "\71"; 84 | } 85 | .datatable-icon-prev:before { 86 | content: "\72"; 87 | } 88 | -------------------------------------------------------------------------------- /i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Access not allowed. Please login.", 3 | "ServerError403": "Access forbidden. Please provide correct credentials", 4 | "ServerError404": "An error occurred. Please contact your administrator", 5 | "ServerError500": "An error occurred. Please contact your administrator", 6 | "SuccessNotificationTitle": "Success", 7 | "ErrorNotificationTitle": "Error", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Warning", 10 | "SaveBtn": "Save", 11 | "EditBtn": "Edit", 12 | "CancelBtn": "Cancel", 13 | "BackBtn": "Back", 14 | "GridEmptyLabel": "No available items", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Please confirm", 18 | "Content": "Do you want to leave without saving the changes?", 19 | "SubmitBtn": "Yes", 20 | "CancelBtn": "No" 21 | } 22 | , 23 | "Auth": { 24 | "Email": "Email", 25 | "Password": "Password", 26 | "ConfirmPassword": "Confirm Password", 27 | "EmailFormatError": "Email is not in valid format", 28 | "EmailRequiredError": "Email is required", 29 | "PasswordRequiredError": "Password is required", 30 | "ConfirmPasswordError": "Passwords mismatch", 31 | "PasswordLengthError": "Passwords must contain at least 6 characters", 32 | 33 | "Login": { 34 | "Title": "Login", 35 | "Submit": "Login", 36 | "RegisterLink": "Sign Up" 37 | }, 38 | 39 | "Register": { 40 | "Title": "Sign Up", 41 | "Submit": "Create account", 42 | "Back": "Go Back", 43 | "SuccessMessage": "Your account has been successfully created" 44 | } 45 | } 46 | , 47 | "Products": { 48 | "Title": "Products" 49 | }, 50 | "ProductDetails": { 51 | "Title": "Product details", 52 | "Name": "Name", 53 | "SerialNumber": "Serial Number", 54 | "Category": "Category", 55 | "Description": "Description", 56 | "WarrantyExpiration": "Warranty Expiration", 57 | "Price": "Price", 58 | "Currency": "Currency" 59 | } 60 | , 61 | "Sidebar": { 62 | "Menu": "Menu", 63 | "ProductsItem": "Products" 64 | }, 65 | 66 | "ProfileActionBar": { 67 | "Logout": "Logout" 68 | }, 69 | 70 | "PageNotFound": { 71 | "Title": "Ooops, Page Not Found", 72 | "Subtitle": "Please, return to the previous page", 73 | "Button": "Go back" 74 | } 75 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular architecture patterns 2 | ============================= 3 | 4 | This application represents a demo project for **Angular architecture patterns** blog series at [http://netmedia.io](http://netmedia.io/blog/angular-architecture-patterns-high-level-project-architecture_5589). 5 | Frontend app is generated with [Angular CLI](https://github.com/angular/angular-cli). It uses it's own local dev server on `http://localhost:4200/`. 6 | 7 | ### Installation 8 | ``` 9 | git clone https://github.com/anteburazer/angular-architecture-patterns.git 10 | cd angular-architecture-patterns 11 | npm install 12 | npm run start 13 | ``` 14 | 15 | 16 | ### Run Development 17 | Run `npm run start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 18 | The command will run custom hooks which will set environment to development and merge all i18n files needed for multi language support. The application uses proxy file to connect with the API. Proxy settings are defined in `proxy.conf.json` 19 | 20 | 21 | ### Build for production 22 | Run `npm run sy-build` to build the application for production which includes tree shaking, AOT and other cool stuff for minification. 23 | This command is defined in `package.json` file under the scripts section and includes regular Angular CLI build command, custom made hooks and generation of service worker file. 24 | 25 | 26 | When application is built for production it's copied in `/dist` folder which is the public folder for **Angular CLI**. 27 | 28 | 29 | ### Hooks 30 | Hooks are located in `/hooks` folder and they are responsible for merging and copying configuration and localization files for development and production. 31 | 32 | #### Note 33 | Copying files is not necessary on angular-cli v1.0.4 and above because it has built in login for this action. You just need to specify which files/folders need to be copied into your destination folder (default `dist`) and you can do that in `.angular-cli.json` file by specifing the assets array: 34 | 35 | ``` 36 | "assets": [ 37 | "assets", 38 | "favicon.ico", 39 | "service-worker.js", 40 | { "glob": "**/*", "input": "../config", "output": "./config/" }, 41 | { "glob": "en.json", "input": "../i18n", "output": "./i18n/" }, 42 | { "glob": "hr.json", "input": "../i18n", "output": "./i18n/" } 43 | ] 44 | ``` -------------------------------------------------------------------------------- /src/app/auth/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit 4 | } from '@angular/core'; 5 | import { 6 | FormBuilder, 7 | FormGroup, 8 | Validators, 9 | AbstractControl 10 | } from '@angular/forms'; 11 | import { 12 | moveIn, 13 | fallIn 14 | } from '../../shared/animations'; 15 | import { AuthSandbox } from '../auth.sandbox'; 16 | 17 | @Component({ 18 | selector: 'register', 19 | templateUrl: './register.component.html', 20 | styleUrls: ['./register.component.scss'], 21 | animations: [moveIn(), fallIn()], 22 | host: {'[@moveIn]': ''} 23 | }) 24 | export class RegisterComponent { 25 | public submitted: boolean = false; 26 | public email: AbstractControl; 27 | public password: AbstractControl; 28 | public confirmPassword: AbstractControl; 29 | public registerForm: FormGroup; 30 | private validators; 31 | 32 | constructor( 33 | private fb: FormBuilder, 34 | public authSandbox: AuthSandbox 35 | ) { 36 | this.validators = authSandbox.validationService; 37 | } 38 | 39 | ngOnInit() { 40 | this.initRegisterForm(); 41 | } 42 | 43 | /** 44 | * Builds a form instance (using FormBuilder) with corresponding validation rules 45 | */ 46 | public initRegisterForm(): void { 47 | this.registerForm = this.fb.group( 48 | { 49 | email: ['', [Validators.required, this.validators.validateEmail]], 50 | password: ['', [Validators.required, Validators.minLength(6)]], 51 | confirmPassword: ['', [Validators.required, Validators.minLength(6)]] 52 | }, 53 | { validator: this.validators.matchingPasswords('password', 'confirmPassword') } 54 | ); 55 | 56 | this.email = this.registerForm.controls['email']; 57 | this.password = this.registerForm.controls['password']; 58 | this.confirmPassword = this.registerForm.controls['confirmPassword']; 59 | } 60 | 61 | /** 62 | * Handles form 'submit' event. Calls sandbox register function if form is valid. 63 | * 64 | * @param event 65 | * @param form 66 | */ 67 | public onSubmit(event: Event, form: any): void { 68 | event.stopPropagation(); 69 | this.submitted = true; 70 | 71 | if (this.registerForm.valid) this.authSandbox.register(form); 72 | } 73 | } -------------------------------------------------------------------------------- /i18n/hr.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerError401": "Pristup nije dozvoljen. Molimo prijavite se.", 3 | "ServerError403": "Pristup nije dozvoljen. Molimo unesite ispravne podatke.", 4 | "ServerError404": "Dogodila se greška. Molimo kontaktirajte svog administratora.", 5 | "ServerError500": "Dogodila se greška. Molimo kontaktirajte svog administratora.", 6 | "SuccessNotificationTitle": "Uspješno", 7 | "ErrorNotificationTitle": "Greška", 8 | "InfoNotificationTitle": "Info", 9 | "WarningNotificationTitle": "Upozorenje", 10 | "SaveBtn": "Spremi", 11 | "EditBtn": "Uredi", 12 | "CancelBtn": "Odustani", 13 | "BackBtn": "Nazad", 14 | "GridEmptyLabel": "Nema dostupnih rezultata", 15 | 16 | "ConfirmDialog": { 17 | "Title": "Molimo potvrdite", 18 | "Content": "Da li želite zatvoriti formu bez spremanja promjena?", 19 | "SubmitBtn": "Da", 20 | "CancelBtn": "Ne" 21 | } 22 | , 23 | "Auth": { 24 | "Email": "Email", 25 | "Password": "Lozinka", 26 | "ConfirmPassword": "Potvrdi lozinku", 27 | "EmailFormatError": "Email adresa nije u pravilnom formatu", 28 | "EmailRequiredError": "Email adresa je obavezna", 29 | "PasswordRequiredError": "Lozinka je obavezna", 30 | "ConfirmPasswordError": "Lozinke se ne podudaraju", 31 | "PasswordLengthError": "Lozinka mora sadržavati najmanje 6 znakova", 32 | 33 | "Login": { 34 | "Title": "Prijavi se", 35 | "Submit": "Prijavi se", 36 | "RegisterLink": "Napravi račun" 37 | }, 38 | 39 | "Register": { 40 | "Title": "Napravi račun", 41 | "Submit": "Napravi račun", 42 | "Back": "Natrag", 43 | "SuccessMessage": "Vaš račun je uspješno kreiran" 44 | } 45 | } 46 | , 47 | "Products": { 48 | "Title": "Proizvodi" 49 | }, 50 | "ProductDetails": { 51 | "Title": "Detalji proizvoda", 52 | "Name": "Naziv", 53 | "SerialNumber": "Serijski Broj", 54 | "Category": "Kategorija", 55 | "Description": "Opis", 56 | "WarrantyExpiration": "Istek Garancije", 57 | "Price": "Cijena", 58 | "Currency": "Valuta" 59 | } 60 | , 61 | "Sidebar": { 62 | "Menu": "Menu", 63 | "ProductsItem": "Proizvodi" 64 | }, 65 | 66 | "ProfileActionBar": { 67 | "Logout": "Odjava" 68 | }, 69 | 70 | "PageNotFound": { 71 | "Title": "Ooops, Stranica Nije Pronađena", 72 | "Subtitle": "Molimo, vratite se na prethodnu stranicu", 73 | "Button": "Povratak" 74 | } 75 | } -------------------------------------------------------------------------------- /src/app/products/products.sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Store } from '@ngrx/store'; 3 | import { Subscription } from "rxjs"; 4 | import { Sandbox } from '../shared/sandbox/base.sandbox'; 5 | import { ProductsApiClient } from './productsApiClient.service'; 6 | import * as store from '../shared/store'; 7 | import * as productsActions from '../shared/store/actions/products.action'; 8 | import * as productDetailsActions from '../shared/store/actions/product-details.action'; 9 | import { 10 | Product, 11 | User 12 | } from '../shared/models'; 13 | 14 | @Injectable() 15 | export class ProductsSandbox extends Sandbox { 16 | 17 | public products$ = this.appState$.select(store.getProductsData); 18 | public productsLoading$ = this.appState$.select(store.getProductsLoading); 19 | public productDetails$ = this.appState$.select(store.getProductDetailsData); 20 | public productDetailsLoading$ = this.appState$.select(store.getProductDetailsLoading); 21 | public loggedUser$ = this.appState$.select(store.getLoggedUser); 22 | 23 | private subscriptions: Array = []; 24 | 25 | constructor( 26 | protected appState$: Store, 27 | private productsApiClient: ProductsApiClient 28 | ) { 29 | super(appState$); 30 | this.registerEvents(); 31 | } 32 | 33 | /** 34 | * Loads products from the server 35 | */ 36 | public loadProducts(): void { 37 | this.appState$.dispatch(new productsActions.LoadAction()) 38 | } 39 | 40 | /** 41 | * Loads product details from the server 42 | */ 43 | public loadProductDetails(id: number): void { 44 | this.appState$.dispatch(new productDetailsActions.LoadAction(id)) 45 | } 46 | 47 | /** 48 | * Dispatches an action to select product details 49 | */ 50 | public selectProduct(product: Product): void { 51 | this.appState$.dispatch(new productDetailsActions.LoadSuccessAction(product)) 52 | } 53 | 54 | /** 55 | * Unsubscribes from events 56 | */ 57 | public unregisterEvents() { 58 | this.subscriptions.forEach(sub => sub.unsubscribe()); 59 | } 60 | 61 | /** 62 | * Subscribes to events 63 | */ 64 | private registerEvents(): void { 65 | // Subscribes to culture 66 | this.subscriptions.push(this.culture$.subscribe((culture: string) => this.culture = culture)); 67 | 68 | this.subscriptions.push(this.loggedUser$.subscribe((user: User) => { 69 | if (user.isLoggedIn) this.loadProducts(); 70 | })) 71 | } 72 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-architecture-patterns", 3 | "version": "1.0.0", 4 | "description": "Demo application for Angular Architecture Patterns blog series on http://netmedia.io/blog/angular-architecture-patterns-high-level-project-architecture_5589", 5 | "license": "MIT", 6 | "author": "Ante Burazer ", 7 | "angular-cli": {}, 8 | "scripts": { 9 | "start": "npm run sy-pre-start & ng serve --proxy-config proxy.conf.json", 10 | "lint": "ng lint", 11 | "test": "npm run sy-pre-test & ng test", 12 | "pree2e": "webdriver-manager update --standalone false --gecko false", 13 | "e2e": "ng e2e", 14 | "sy-pre-test": "node hooks/pre-test.js", 15 | "sy-pre-start": "node hooks/pre-start.js", 16 | "sy-pre-build": "node hooks/pre-build.js", 17 | "sy-post-build": "node hooks/post-build.js", 18 | "sw": "sw-precache --root=./dist --config=sw-precache-config.js", 19 | "sy-build": "npm run sy-pre-build & ng build --prod --aot & npm run sy-post-build & npm run sw" 20 | }, 21 | "private": true, 22 | "dependencies": { 23 | "@angular/animations": "^4.0.2", 24 | "@angular/common": "^4.0.2", 25 | "@angular/compiler": "^4.0.2", 26 | "@angular/core": "^4.0.2", 27 | "@angular/forms": "^4.0.2", 28 | "@angular/http": "^4.0.2", 29 | "@angular/platform-browser": "^4.0.2", 30 | "@angular/platform-browser-dynamic": "^4.0.2", 31 | "@angular/router": "^4.0.2", 32 | "@ngrx/core": "^1.2.0", 33 | "@ngrx/effects": "^2.0.3", 34 | "@ngrx/store": "^2.2.2", 35 | "@ngrx/store-devtools": "^3.2.4", 36 | "@swimlane/ngx-datatable": "^9.1.0", 37 | "angular2-notifications": "^0.4.53", 38 | "core-js": "2.4.1", 39 | "ng2-popover": "0.0.13", 40 | "ng2-translate": "^5.0.0", 41 | "reselect": "^2.5.4", 42 | "rxjs": "^5.2.0", 43 | "ts-helpers": "1.1.2", 44 | "zone.js": "^0.8.4" 45 | }, 46 | "devDependencies": { 47 | "@angular/cli": "^1.0.4", 48 | "@angular/compiler-cli": "^4.0.2", 49 | "@types/jasmine": "2.5.38", 50 | "@types/node": "^7.0.5", 51 | "codelyzer": "^3.0.0-beta.3", 52 | "fs-extra": "^1.0.0", 53 | "jasmine-core": "^2.5.2", 54 | "json-concat": "0.0.1", 55 | "karma": "^1.5.0", 56 | "karma-chrome-launcher": "^2.0.0", 57 | "karma-cli": "^1.0.1", 58 | "karma-coverage-istanbul-reporter": "^0.3.0", 59 | "karma-jasmine": "^1.1.0", 60 | "karma-jasmine-html-reporter": "^0.2.2", 61 | "protractor": "^5.1.1", 62 | "sw-precache": "^5.0.0", 63 | "sw-toolbox": "^3.6.0", 64 | "ts-node": "^2.1.0", 65 | "tslint": "^4.5.1", 66 | "typescript": "^2.2.2", 67 | "webdriver-manager": "11.1.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/auth/auth.sandbox.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Store } from '@ngrx/store'; 4 | import { Subscription } from "rxjs"; 5 | import { Sandbox } from '../shared/sandbox/base.sandbox'; 6 | import * as store from '../shared/store'; 7 | import * as authActions from '../shared/store/actions/auth.action'; 8 | import { User } from '../shared/models'; 9 | import { 10 | UtilService, 11 | ValidationService 12 | } from '../shared/utility'; 13 | import { 14 | LoginForm, 15 | RegisterForm 16 | } from '../shared/models'; 17 | 18 | @Injectable() 19 | export class AuthSandbox extends Sandbox { 20 | 21 | public loginLoading$ = this.appState$.select(store.getAuthLoading); 22 | public loginLoaded$ = this.appState$.select(store.getAuthLoaded); 23 | public loggedUser$ = this.appState$.select(store.getLoggedUser); 24 | 25 | private subscriptions: Array = []; 26 | 27 | constructor( 28 | private router: Router, 29 | protected appState$: Store, 30 | private utilService: UtilService, 31 | public validationService: ValidationService 32 | ) { 33 | super(appState$); 34 | this.registerAuthEvents(); 35 | } 36 | 37 | /** 38 | * Dispatches login action 39 | * 40 | * @param form 41 | */ 42 | public login(form: any): void { 43 | this.appState$.dispatch(new authActions.DoLoginAction(new LoginForm(form))); 44 | } 45 | 46 | /** 47 | * Dispatches register action 48 | * 49 | * @param form 50 | */ 51 | public register(form: any): void { 52 | this.appState$.dispatch(new authActions.DoRegisterAction(new RegisterForm(form))); 53 | } 54 | 55 | /** 56 | * Unsubscribe from events 57 | */ 58 | public unregisterEvents() { 59 | this.subscriptions.forEach(sub => sub.unsubscribe()); 60 | } 61 | 62 | /** 63 | * Registers events 64 | */ 65 | private registerAuthEvents(): void { 66 | // Subscribes to login success event and redirects user to home page 67 | this.subscriptions.push(this.loginLoaded$.subscribe((loaded: boolean) => { 68 | if (loaded) this.router.navigate(['/products']); 69 | })); 70 | 71 | // Subscribes to logged user data and save/remove it from the local storage 72 | this.subscriptions.push(this.loggedUser$.subscribe((user: User) => { 73 | if (user.isLoggedIn) user.save(); 74 | else user.remove(); 75 | })); 76 | } 77 | 78 | /** 79 | * Uncapitalize response keys 80 | * 81 | * @param user 82 | */ 83 | static authAdapter(user: any): any { 84 | return Object.assign({}, user, { email: user.Email}); 85 | } 86 | } -------------------------------------------------------------------------------- /src/assets/fonts/data-table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by Fontastic.me 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/app/shared/store/effects/products.effect.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/catch'; 2 | import 'rxjs/add/operator/map'; 3 | import 'rxjs/add/operator/switchMap'; 4 | import { Injectable } from '@angular/core'; 5 | import { Effect, Actions } from '@ngrx/effects'; 6 | import { Action } from '@ngrx/store'; 7 | import { Observable } from 'rxjs/Observable'; 8 | import { of } from 'rxjs/observable/of'; 9 | import { ProductsApiClient } from '../../../products/productsApiClient.service'; 10 | import * as productsActions from '../actions/products.action'; 11 | import * as productDetailsActions from '../actions/product-details.action'; 12 | import { Store } from '@ngrx/store'; 13 | import * as store from '../index'; 14 | import { Product } from '../../models'; 15 | 16 | /** 17 | * Effects offer a way to isolate and easily test side-effects within your 18 | * application. StateUpdates is an observable of the latest state and 19 | * dispatched action. The `toPayload` helper function returns just 20 | * the payload of the currently dispatched action, useful in 21 | * instances where the current state is not necessary. 22 | * 23 | * If you are unfamiliar with the operators being used in these examples, please 24 | * check out the sources below: 25 | * 26 | * Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators 27 | * RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35 28 | */ 29 | 30 | @Injectable() 31 | export class ProductsEffects { 32 | 33 | constructor( 34 | private actions$: Actions, 35 | private productsApiClient: ProductsApiClient, 36 | private appState$: Store) {} 37 | 38 | /** 39 | * Product list 40 | */ 41 | @Effect() 42 | getProducts$: Observable = this.actions$ 43 | .ofType(productsActions.ActionTypes.LOAD) 44 | .map((action: productsActions.LoadAction) => action.payload) 45 | .switchMap(state => { 46 | return this.productsApiClient.getProducts() 47 | .map(products => new productsActions.LoadSuccessAction(products)) 48 | .catch(error => of(new productsActions.LoadFailAction())); 49 | }); 50 | 51 | /** 52 | * Product details 53 | */ 54 | @Effect() 55 | getProductDetails$: Observable = this.actions$ 56 | .ofType(productDetailsActions.ActionTypes.LOAD) 57 | .map((action: productDetailsActions.LoadAction) => action.payload) 58 | .switchMap(state => { 59 | return this.productsApiClient.getProductDetails(state) 60 | .map(products => new productDetailsActions.LoadSuccessAction(products)) 61 | .catch(error => of(new productDetailsActions.LoadFailAction())); 62 | }); 63 | } -------------------------------------------------------------------------------- /src/app/shared/store/effects/auth.effect.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/catch'; 2 | import 'rxjs/add/operator/map'; 3 | import 'rxjs/add/operator/switchMap'; 4 | import { Injectable } from '@angular/core'; 5 | import { Effect, Actions } from '@ngrx/effects'; 6 | import { Action } from '@ngrx/store'; 7 | import { Observable } from 'rxjs/Observable'; 8 | import { of } from 'rxjs/observable/of'; 9 | import { AuthApiClient } from '../../../auth/authApiClient.service'; 10 | import * as actions from '../actions/auth.action'; 11 | import { Store } from '@ngrx/store'; 12 | import * as store from '../index'; 13 | import { User } from '../../models'; 14 | 15 | /** 16 | * Effects offer a way to isolate and easily test side-effects within your 17 | * application. StateUpdates is an observable of the latest state and 18 | * dispatched action. The `toPayload` helper function returns just 19 | * the payload of the currently dispatched action, useful in 20 | * instances where the current state is not necessary. 21 | * 22 | * If you are unfamiliar with the operators being used in these examples, please 23 | * check out the sources below: 24 | * 25 | * Official Docs: http://reactivex.io/rxjs/manual/overview.html#categories-of-operators 26 | * RxJS 5 Operators By Example: https://gist.github.com/btroncone/d6cf141d6f2c00dc6b35 27 | */ 28 | 29 | @Injectable() 30 | export class AuthEffects { 31 | 32 | constructor( 33 | private actions$: Actions, 34 | private authApiClient: AuthApiClient, 35 | private appState$: Store) {} 36 | 37 | /** 38 | * Login effect 39 | */ 40 | @Effect() 41 | doLogin$: Observable = this.actions$ 42 | .ofType(actions.ActionTypes.DO_LOGIN) 43 | .map((action: actions.DoLoginAction) => action.payload) 44 | .switchMap(state => { 45 | return this.authApiClient.login(state) 46 | .map(user => new actions.DoLoginSuccessAction(new User(user))) 47 | .catch(error => of(new actions.DoLoginFailAction())); 48 | }); 49 | 50 | /** 51 | * Registers effect 52 | */ 53 | @Effect() 54 | doRegister$: Observable = this.actions$ 55 | .ofType(actions.ActionTypes.DO_REGISTER) 56 | .map((action: actions.DoRegisterAction) => action.payload) 57 | .switchMap(state => { 58 | return this.authApiClient.register(state) 59 | .map(user => new actions.DoRegisterSuccessAction(new User(user))) 60 | .catch(error => of(new actions.DoRegisterFailAction())); 61 | }); 62 | 63 | /** 64 | * Logout effect 65 | */ 66 | @Effect() 67 | doLogout$: Observable = this.actions$ 68 | .ofType(actions.ActionTypes.DO_LOGOUT) 69 | .map((action: actions.DoLogoutAction) => null) 70 | .switchMap(state => { 71 | return this.authApiClient.logout() 72 | .map(() => new actions.DoLogoutSuccessAction()) 73 | .catch(error => of(new actions.DoLogoutFailAction())); 74 | }); 75 | } -------------------------------------------------------------------------------- /src/app/shared/components/loadingPlaceholder/loadingPlaceholder.component.scss: -------------------------------------------------------------------------------- 1 | .timeline-item { 2 | background: #fff; 3 | border-radius: 3px; 4 | padding: 12px; 5 | margin: 0 auto; 6 | min-height: 200px; 7 | } 8 | 9 | @keyframes placeHolderShimmer{ 10 | 0%{ 11 | background-position: -468px 0 12 | } 13 | 100%{ 14 | background-position: 468px 0 15 | } 16 | } 17 | 18 | .animated-background { 19 | animation-duration: 1s; 20 | animation-fill-mode: forwards; 21 | animation-iteration-count: infinite; 22 | animation-name: placeHolderShimmer; 23 | animation-timing-function: linear; 24 | background: #f6f7f8; 25 | background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%); 26 | background-size: 800px 104px; 27 | height: 180px; 28 | position: relative; 29 | } 30 | 31 | .background-masker { 32 | background: #fff; 33 | position: absolute; 34 | } 35 | 36 | /* Every thing below this is just positioning */ 37 | 38 | .background-masker.header-top, 39 | .background-masker.header-bottom, 40 | .background-masker.subheader-bottom { 41 | top: 0; 42 | left: 120px; 43 | right: 0; 44 | height: 10px; 45 | } 46 | 47 | .background-masker.header-left, 48 | .background-masker.subheader-left, 49 | .background-masker.header-right, 50 | .background-masker.subheader-right { 51 | top: 10px; 52 | left: 120px; 53 | height: 12px; 54 | width: 10px; 55 | } 56 | 57 | .background-masker.header-bottom { 58 | top: 22px; 59 | height: 12px; 60 | } 61 | 62 | .background-masker.subheader-left, 63 | .background-masker.subheader-right { 64 | top: 34px; 65 | height: 6px; 66 | } 67 | 68 | 69 | .background-masker.header-right, 70 | .background-masker.subheader-right { 71 | width: auto; 72 | left: 50%; 73 | right: 0; 74 | } 75 | 76 | .background-masker.subheader-right { 77 | left: 30%; 78 | } 79 | 80 | .background-masker.subheader-bottom { 81 | top: 40px; 82 | height: 60px; 83 | } 84 | 85 | .background-masker.content-top, 86 | .background-masker.content-second-line, 87 | .background-masker.content-third-line, 88 | .background-masker.content-second-end, 89 | .background-masker.content-third-end, 90 | .background-masker.content-first-end { 91 | top: 100px; 92 | left: 0; 93 | right: 0; 94 | height: 6px; 95 | } 96 | 97 | .background-masker.content-top { 98 | height:34px; 99 | } 100 | 101 | .background-masker.content-first-end, 102 | .background-masker.content-second-end, 103 | .background-masker.content-third-end{ 104 | width: auto; 105 | left: 70%; 106 | right: 0; 107 | top: 134px; 108 | height: 12px; 109 | } 110 | 111 | .background-masker.content-second-line { 112 | top: 146px; 113 | } 114 | 115 | .background-masker.content-second-end { 116 | left: 80%; 117 | top: 152px; 118 | } 119 | 120 | .background-masker.content-third-line { 121 | top: 162px; 122 | } 123 | 124 | .background-masker.content-third-end { 125 | left: 60%; 126 | top: 168px; 127 | } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/css/fontello-ie7-codes.css: -------------------------------------------------------------------------------- 1 | 2 | .icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 3 | .icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 4 | .icon-th-large { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 5 | .icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 6 | .icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 7 | .icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 8 | .icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 9 | .icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 10 | .icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 11 | .icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 12 | .icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 13 | .icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 14 | .icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 15 | .icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 16 | .icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 17 | .icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 18 | .icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 19 | .icon-box { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 20 | .icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 21 | .icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 22 | .icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 23 | .icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 24 | .icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 25 | .icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 26 | .icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 27 | .icon-ellipsis { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 28 | .icon-user-circle-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 29 | .icon-user-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -------------------------------------------------------------------------------- /src/app/products/product-details.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | OnInit, 4 | OnDestroy, 5 | ChangeDetectionStrategy, 6 | ChangeDetectorRef 7 | } from '@angular/core'; 8 | import { ActivatedRoute } from '@angular/router'; 9 | import { Subscription } from "rxjs"; 10 | import { ProductsSandbox } from './products.sandbox'; 11 | import { Product } from '../shared/models'; 12 | 13 | @Component({ 14 | selector: 'app-product-details', 15 | template: ` 16 | 17 |

{{ 'ProductDetails.Title' | translate }}

18 |
19 |

{{ product?.name }}

20 | 21 |
22 |
23 |
{{ 'ProductDetails.SerialNumber' | translate }}
24 |

{{ product?.serialNumber }}

25 |
26 | 27 |
28 |
{{ 'ProductDetails.Category' | translate }}
29 |

{{ product?.category }}

30 |
31 | 32 |
33 |
{{ 'ProductDetails.Description' | translate }}
34 |

{{ product?.description }}

35 |
36 | 37 |
38 |
{{ 'ProductDetails.WarrantyExpiration' | translate }}
39 |

{{ productsSandbox.formatDate(product?.warrantyExpiration) }}

40 |
41 | 42 |
43 |
{{ 'ProductDetails.Price' | translate }}
44 |

{{ product?.price }} {{ product?.currency }}

45 |
46 |
47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 |
57 |
58 | `, 59 | styles: ['.productDetails-contentWrapper { margin-top: 20px; } .productDetails-backBtn { position: absolute; top: 20px; right: 20px; }'], 60 | changeDetection: ChangeDetectionStrategy.OnPush 61 | }) 62 | export class ProductDetailsComponent implements OnInit, OnDestroy { 63 | 64 | public product: Product; 65 | private subscriptions: Array = []; 66 | 67 | constructor( 68 | public productsSandbox: ProductsSandbox, 69 | private changeDetector: ChangeDetectorRef, 70 | private activatedRoute: ActivatedRoute) {} 71 | 72 | ngOnInit() { 73 | this.registerEvents(); 74 | } 75 | 76 | ngOnDestroy() { 77 | this.subscriptions.forEach(sub => sub.unsubscribe()); 78 | } 79 | 80 | /** 81 | * Registers events 82 | */ 83 | private registerEvents(): void { 84 | // Subscribes to product details 85 | this.subscriptions.push(this.productsSandbox.productDetails$.subscribe((product: any) => { 86 | if (product) { 87 | this.changeDetector.markForCheck(); 88 | this.product = product; 89 | } 90 | })); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licenses, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publicly available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with , newValues: Array) => boolean; 8 | 9 | /** 10 | * This function coerces a string into a string literal type. 11 | * Using tagged union types in TypeScript 2.0, this enables 12 | * powerful typechecking of our reducers. 13 | * 14 | * Since every action label passes through this function it 15 | * is a good place to ensure all of our action labels are unique. 16 | * 17 | * @param label 18 | */ 19 | export function type(label: T | ''): T { 20 | if (typeCache[label]) { 21 | throw new Error(`Action type "${label}" is not unqiue"`); 22 | } 23 | 24 | typeCache[label] = true; 25 | 26 | return label; 27 | } 28 | 29 | /** 30 | * Runs through every condition, compares new and old values and returns true/false depends on condition state. 31 | * This is used to distinct if two observable values have changed. 32 | * 33 | * @param oldValues 34 | * @param newValues 35 | * @param conditions 36 | */ 37 | export function distinctChanges(oldValues: Array, newValues: Array, conditions: Predicate[]): boolean { 38 | if (conditions.every(cond => cond(oldValues, newValues))) return false; 39 | return true; 40 | } 41 | 42 | /** 43 | * Returns true if the given value is type of Object 44 | * 45 | * @param val 46 | */ 47 | export function isObject(val: any) { 48 | if (val === null) return false; 49 | 50 | return ( (typeof val === 'function') || (typeof val === 'object') ); 51 | } 52 | 53 | /** 54 | * Capitalizes the first character in given string 55 | * 56 | * @param s 57 | */ 58 | export function capitalize(s: string) { 59 | if (!s || typeof s !== 'string') return s; 60 | return s && s[0].toUpperCase() + s.slice(1); 61 | } 62 | 63 | /** 64 | * Uncapitalizes the first character in given string 65 | * 66 | * @param s 67 | */ 68 | export function uncapitalize(s: string) { 69 | if (!s || typeof s !== 'string') return s; 70 | return s && s[0].toLowerCase() + s.slice(1); 71 | } 72 | 73 | /** 74 | * Flattens multi dimensional object into one level deep 75 | * 76 | * @param obj 77 | * @param preservePath 78 | */ 79 | export function flattenObject(ob: any, preservePath: boolean = false): any { 80 | var toReturn = {}; 81 | 82 | for (var i in ob) { 83 | if (!ob.hasOwnProperty(i)) continue; 84 | 85 | if ((typeof ob[i]) == 'object') { 86 | var flatObject = flattenObject(ob[i], preservePath); 87 | for (var x in flatObject) { 88 | if (!flatObject.hasOwnProperty(x)) continue; 89 | 90 | let path = preservePath ? (i + '.' + x) : x; 91 | 92 | toReturn[path] = flatObject[x]; 93 | } 94 | } else toReturn[i] = ob[i]; 95 | } 96 | 97 | return toReturn; 98 | } 99 | 100 | /** 101 | * Returns formated date based on given culture 102 | * 103 | * @param dateString 104 | * @param culture 105 | */ 106 | export function localeDateString(dateString: string, culture: string = 'en-EN'): string { 107 | let date = new Date(dateString); 108 | return date.toLocaleDateString(culture); 109 | } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/css/fontello-ie7.css: -------------------------------------------------------------------------------- 1 | [class^="icon-"], [class*=" icon-"] { 2 | font-family: 'fontello'; 3 | font-style: normal; 4 | font-weight: normal; 5 | 6 | /* fix buttons height */ 7 | line-height: 1em; 8 | 9 | /* you can be more comfortable with increased icons size */ 10 | /* font-size: 120%; */ 11 | } 12 | 13 | .icon-mail { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 14 | .icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 15 | .icon-th-large { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 16 | .icon-th-list { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 17 | .icon-ok { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 18 | .icon-eye { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 19 | .icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 20 | .icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 21 | .icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 22 | .icon-calendar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 23 | .icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 24 | .icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 25 | .icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 26 | .icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 27 | .icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 28 | .icon-comment { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 29 | .icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 30 | .icon-box { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 31 | .icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 32 | .icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 33 | .icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 34 | .icon-angle-left { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 35 | .icon-angle-right { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 36 | .icon-angle-up { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 37 | .icon-angle-down { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 38 | .icon-ellipsis { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 39 | .icon-user-circle-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } 40 | .icon-user-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/css/fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../font/fontello.eot?58763546'); 4 | src: url('../font/fontello.eot?58763546#iefix') format('embedded-opentype'), 5 | url('../font/fontello.woff2?58763546') format('woff2'), 6 | url('../font/fontello.woff?58763546') format('woff'), 7 | url('../font/fontello.ttf?58763546') format('truetype'), 8 | url('../font/fontello.svg?58763546#fontello') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 14 | /* 15 | @media screen and (-webkit-min-device-pixel-ratio:0) { 16 | @font-face { 17 | font-family: 'fontello'; 18 | src: url('../font/fontello.svg?58763546#fontello') format('svg'); 19 | } 20 | } 21 | */ 22 | 23 | [class^="icon-"]:before, [class*=" icon-"]:before { 24 | font-family: "fontello"; 25 | font-style: normal; 26 | font-weight: normal; 27 | speak: none; 28 | 29 | display: inline-block; 30 | text-decoration: inherit; 31 | width: 1em; 32 | margin-right: .2em; 33 | text-align: center; 34 | /* opacity: .8; */ 35 | 36 | /* For safety - reset parent styles, that can break glyph codes*/ 37 | font-variant: normal; 38 | text-transform: none; 39 | 40 | /* fix buttons height, for twitter bootstrap */ 41 | line-height: 1em; 42 | 43 | /* Animation center compensation - margins should be symmetric */ 44 | /* remove if not needed */ 45 | margin-left: .2em; 46 | 47 | /* you can be more comfortable with increased icons size */ 48 | /* font-size: 120%; */ 49 | 50 | /* Font smoothing. That was taken from TWBS */ 51 | -webkit-font-smoothing: antialiased; 52 | -moz-osx-font-smoothing: grayscale; 53 | 54 | /* Uncomment for 3D effect */ 55 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 56 | } 57 | 58 | .icon-mail:before { content: '\e800'; } /* '' */ 59 | .icon-user:before { content: '\e801'; } /* '' */ 60 | .icon-th-large:before { content: '\e802'; } /* '' */ 61 | .icon-th-list:before { content: '\e803'; } /* '' */ 62 | .icon-ok:before { content: '\e804'; } /* '' */ 63 | .icon-eye:before { content: '\e805'; } /* '' */ 64 | .icon-bell:before { content: '\e806'; } /* '' */ 65 | .icon-cog:before { content: '\e807'; } /* '' */ 66 | .icon-wrench:before { content: '\e808'; } /* '' */ 67 | .icon-calendar:before { content: '\e809'; } /* '' */ 68 | .icon-logout:before { content: '\e80a'; } /* '' */ 69 | .icon-down-open:before { content: '\e80b'; } /* '' */ 70 | .icon-up-open:before { content: '\e80c'; } /* '' */ 71 | .icon-right-open:before { content: '\e80d'; } /* '' */ 72 | .icon-left-open:before { content: '\e80e'; } /* '' */ 73 | .icon-comment:before { content: '\e80f'; } /* '' */ 74 | .icon-chart-bar:before { content: '\e810'; } /* '' */ 75 | .icon-box:before { content: '\e811'; } /* '' */ 76 | .icon-mail-alt:before { content: '\f0e0'; } /* '' */ 77 | .icon-comment-empty:before { content: '\f0e5'; } /* '' */ 78 | .icon-bell-alt:before { content: '\f0f3'; } /* '' */ 79 | .icon-angle-left:before { content: '\f104'; } /* '' */ 80 | .icon-angle-right:before { content: '\f105'; } /* '' */ 81 | .icon-angle-up:before { content: '\f106'; } /* '' */ 82 | .icon-angle-down:before { content: '\f107'; } /* '' */ 83 | .icon-ellipsis:before { content: '\f141'; } /* '' */ 84 | .icon-user-circle-o:before { content: '\f2be'; } /* '' */ 85 | .icon-user-o:before { content: '\f2c0'; } /* '' */ -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/http.decorator.ts: -------------------------------------------------------------------------------- 1 | import { RequestMethod } from "@angular/http"; 2 | import { 3 | HttpService, 4 | MediaType 5 | } from './http.service'; 6 | import { 7 | methodBuilder, 8 | paramBuilder 9 | } from './utils.service'; 10 | 11 | /* ********************************************* 12 | * Class decorators 13 | * *********************************************/ 14 | 15 | /** 16 | * Set the base URL of REST resource 17 | * @param {String} url - base URL 18 | */ 19 | export function BaseUrl(url: string) { 20 | return function (Target: TFunction): TFunction { 21 | Target.prototype.getBaseUrl = () => url; 22 | return Target; 23 | }; 24 | } 25 | 26 | /** 27 | * Set default headers for every method of the HttpService 28 | * @param {Object} headers - deafult headers in a key-value pair 29 | */ 30 | export function DefaultHeaders(headers: any) { 31 | return function (Target: TFunction): TFunction { 32 | Target.prototype.getDefaultHeaders = () => headers; 33 | return Target; 34 | }; 35 | } 36 | 37 | 38 | /* ********************************************* 39 | * Method decorators 40 | * *********************************************/ 41 | 42 | /** 43 | * GET method 44 | * @param {string} url - resource url of the method 45 | */ 46 | export var GET = methodBuilder(RequestMethod.Get); 47 | /** 48 | * POST method 49 | * @param {string} url - resource url of the method 50 | */ 51 | export var POST = methodBuilder(RequestMethod.Post); 52 | /** 53 | * PUT method 54 | * @param {string} url - resource url of the method 55 | */ 56 | export var PUT = methodBuilder(RequestMethod.Put); 57 | /** 58 | * DELETE method 59 | * @param {string} url - resource url of the method 60 | */ 61 | export var DELETE = methodBuilder(RequestMethod.Delete); 62 | /** 63 | * HEAD method 64 | * @param {string} url - resource url of the method 65 | */ 66 | export var HEAD = methodBuilder(RequestMethod.Head); 67 | 68 | /** 69 | * Set custom headers for a REST method 70 | * @param {Object} headersDef - custom headers in a key-value pair 71 | */ 72 | export function Headers(headersDef: any) { 73 | return function(target: HttpService, propertyKey: string, descriptor: any) { 74 | descriptor.headers = headersDef; 75 | return descriptor; 76 | }; 77 | } 78 | 79 | /** 80 | * Defines the media type(s) that the methods can produce 81 | * @param MediaType producesDef - MediaType to be sent 82 | */ 83 | export function Produces(producesDef: MediaType) { 84 | return function(target: HttpService, propertyKey: string, descriptor: any) { 85 | descriptor.isJSON = producesDef === MediaType.JSON; 86 | descriptor.isFormData = producesDef === MediaType.FORM_DATA; 87 | return descriptor; 88 | }; 89 | } 90 | 91 | /** 92 | * Defines the adatper function to modify the API response suitable for the app 93 | * @param TFunction adapterFn - function to be called 94 | */ 95 | export function Adapter(adapterFn: Function) { 96 | return function(target: HttpService, propertyKey: string, descriptor: any) { 97 | descriptor.adapter = adapterFn || null; 98 | return descriptor; 99 | }; 100 | } 101 | 102 | 103 | /* ********************************************* 104 | * Parameter decorators 105 | * *********************************************/ 106 | 107 | /** 108 | * Path variable of a method's url, type: string 109 | * @param {string} key - path key to bind value 110 | */ 111 | export var Path = paramBuilder("Path"); 112 | /** 113 | * Query value of a method's url, type: string 114 | * @param {string} key - query key to bind value 115 | */ 116 | export var Query = paramBuilder("Query"); 117 | /** 118 | * Body of a REST method, type: key-value pair object 119 | * Only one body per method! 120 | */ 121 | export var Body = paramBuilder("Body")("Body"); 122 | /** 123 | * Custom header of a REST method, type: string 124 | * @param {string} key - header key to bind value 125 | */ 126 | export var Header = paramBuilder("Header"); -------------------------------------------------------------------------------- /src/app/shared/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | /** 4 | * More info: https://egghead.io/lessons/javascript-redux-implementing-combinereducers-from-scratch 5 | */ 6 | import { ActionReducer, combineReducers } from '@ngrx/store'; 7 | 8 | /** 9 | * More info: https://drboolean.gitbooks.io/mostly-adequate-guide/content/ch5.html 10 | */ 11 | import { compose } from '@ngrx/core/compose'; 12 | 13 | /** 14 | * Every reducer module's default export is the reducer function itself. In 15 | * addition, each module should export a type or interface that describes 16 | * the state of the reducer plus any selector functions. The `* as` 17 | * notation packages up all of the exports into a single object. 18 | */ 19 | import * as fromSettings from './reducers/settings.reducer'; 20 | import * as fromAuth from './reducers/auth.reducer'; 21 | import * as fromProducts from './reducers/products.reducer'; 22 | import * as fromProductDetails from './reducers/product-details.reducer'; 23 | 24 | /** 25 | * We treat each reducer like a table in a database. This means 26 | * our top level state interface is just a map of keys to inner state types. 27 | */ 28 | export interface State { 29 | settings: fromSettings.State; 30 | login: fromAuth.State; 31 | products: fromProducts.State; 32 | productDetails: fromProductDetails.State; 33 | } 34 | 35 | /** 36 | * Because metareducers take a reducer function and return a new reducer, 37 | * we can use our compose helper to chain them together. Here we are 38 | * using combineReducers to make our top level reducer, and then 39 | * wrapping that in storeLogger. Remember that compose applies 40 | * the result from right to left. 41 | */ 42 | const reducers = { 43 | settings: fromSettings.reducer, 44 | login: fromAuth.reducer, 45 | products: fromProducts.reducer, 46 | productDetails: fromProductDetails.reducer 47 | }; 48 | 49 | export function store(state: any, action: any) { 50 | const store: ActionReducer = compose(combineReducers)(reducers); 51 | return store(state, action); 52 | } 53 | 54 | /** 55 | * Every reducer module exports selector functions, however child reducers 56 | * have no knowledge of the overall state tree. To make them useable, we 57 | * need to make new selectors that wrap them. 58 | */ 59 | 60 | /** 61 | * Settings store functions 62 | */ 63 | export const getSettingsState = (state: State) => state.settings; 64 | export const getSelectedLanguage = createSelector(getSettingsState, fromSettings.getSelectedLanguage); 65 | export const getSelectedCulture = createSelector(getSettingsState, fromSettings.getSelectedCulture); 66 | export const getAvailableLanguages = createSelector(getSettingsState, fromSettings.getAvailableLanguages); 67 | 68 | /** 69 | * Auth store functions 70 | */ 71 | export const getAuthState = (state: State) => state.login; 72 | export const getAuthLoaded = createSelector(getAuthState, fromAuth.getLoaded); 73 | export const getAuthLoading = createSelector(getAuthState, fromAuth.getLoading); 74 | export const getAuthFailed = createSelector(getAuthState, fromAuth.getFailed); 75 | export const getLoggedUser = createSelector(getAuthState, fromAuth.getLoggedUser); 76 | 77 | /** 78 | * Products store functions 79 | */ 80 | export const getProductsState = (state: State) => state.products; 81 | export const getProductsLoaded = createSelector(getProductsState, fromProducts.getLoaded); 82 | export const getProductsLoading = createSelector(getProductsState, fromProducts.getLoading); 83 | export const getProductsFailed = createSelector(getProductsState, fromProducts.getFailed); 84 | export const getProductsData = createSelector(getProductsState, fromProducts.getData); 85 | 86 | /** 87 | * Product details store functions 88 | */ 89 | export const getProductDetailsState = (state: State) => state.productDetails; 90 | export const getProductDetailsLoaded = createSelector(getProductDetailsState, fromProductDetails.getLoaded); 91 | export const getProductDetailsLoading = createSelector(getProductDetailsState, fromProductDetails.getLoading); 92 | export const getProductDetailsFailed = createSelector(getProductDetailsState, fromProductDetails.getFailed); 93 | export const getProductDetailsData = createSelector(getProductDetailsState, fromProductDetails.getData); -------------------------------------------------------------------------------- /src/styles/margins.scss: -------------------------------------------------------------------------------- 1 | .m-xxs { 2 | margin: 2px 4px; 3 | } 4 | 5 | .m-xs { 6 | margin: 5px; 7 | } 8 | 9 | .m-sm { 10 | margin: 10px; 11 | } 12 | 13 | .m { 14 | margin: 15px; 15 | } 16 | 17 | .m-md { 18 | margin: 20px; 19 | } 20 | 21 | .m-lg { 22 | margin: 30px; 23 | } 24 | 25 | .m-xl { 26 | margin: 50px; 27 | } 28 | 29 | .m-n { 30 | margin: 0 !important; 31 | } 32 | 33 | .m-l-none { 34 | margin-left: 0; 35 | } 36 | 37 | .m-l-xs { 38 | margin-left: 5px; 39 | } 40 | 41 | .m-l-sm { 42 | margin-left: 10px; 43 | } 44 | 45 | .m-l { 46 | margin-left: 15px; 47 | } 48 | 49 | .m-l-md { 50 | margin-left: 20px; 51 | } 52 | 53 | .m-l-lg { 54 | margin-left: 30px; 55 | } 56 | 57 | .m-l-xl { 58 | margin-left: 40px; 59 | } 60 | 61 | .m-l-xxl { 62 | margin-left: 50px; 63 | } 64 | 65 | .m-l-n-xxs { 66 | margin-left: -1px; 67 | } 68 | 69 | .m-l-n-xs { 70 | margin-left: -5px; 71 | } 72 | 73 | .m-l-n-sm { 74 | margin-left: -10px; 75 | } 76 | 77 | .m-l-n { 78 | margin-left: -15px; 79 | } 80 | 81 | .m-l-n-md { 82 | margin-left: -20px; 83 | } 84 | 85 | .m-l-n-lg { 86 | margin-left: -30px; 87 | } 88 | 89 | .m-l-n-xl { 90 | margin-left: -40px; 91 | } 92 | 93 | .m-l-n-xxl { 94 | margin-left: -50px; 95 | } 96 | 97 | .m-t-none { 98 | margin-top: 0; 99 | } 100 | 101 | .m-t-xxs { 102 | margin-top: 1px; 103 | } 104 | 105 | .m-t-xs { 106 | margin-top: 5px; 107 | } 108 | 109 | .m-t-sm { 110 | margin-top: 10px; 111 | } 112 | 113 | .m-t { 114 | margin-top: 15px; 115 | } 116 | 117 | .m-t-md { 118 | margin-top: 20px; 119 | } 120 | 121 | .m-t-lg { 122 | margin-top: 30px; 123 | } 124 | 125 | .m-t-xl { 126 | margin-top: 40px; 127 | } 128 | 129 | .m-t-xxl { 130 | margin-top: 50px; 131 | } 132 | 133 | .m-t-n-xxs { 134 | margin-top: -1px; 135 | } 136 | 137 | .m-t-n-xs { 138 | margin-top: -5px!important; 139 | } 140 | 141 | .m-t-n-sm { 142 | margin-top: -10px; 143 | } 144 | 145 | .m-t-n { 146 | margin-top: -15px; 147 | } 148 | 149 | .m-t-n-md { 150 | margin-top: -20px; 151 | } 152 | 153 | .m-t-n-lg { 154 | margin-top: -30px; 155 | } 156 | 157 | .m-t-n-xl { 158 | margin-top: -40px; 159 | } 160 | 161 | .m-t-n-xxl { 162 | margin-top: -50px; 163 | } 164 | 165 | .m-r-none { 166 | margin-right: 0; 167 | } 168 | 169 | .m-r-xxs { 170 | margin-right: 1px; 171 | } 172 | 173 | .m-r-xs { 174 | margin-right: 5px; 175 | } 176 | 177 | .m-r-sm { 178 | margin-right: 10px; 179 | } 180 | 181 | .m-r { 182 | margin-right: 15px; 183 | } 184 | 185 | .m-r-md { 186 | margin-right: 20px; 187 | } 188 | 189 | .m-r-lg { 190 | margin-right: 30px; 191 | } 192 | 193 | .m-r-xl { 194 | margin-right: 40px; 195 | } 196 | 197 | .m-r-xxl { 198 | margin-right: 50px; 199 | } 200 | 201 | .m-r-n-xxs { 202 | margin-right: -1px; 203 | } 204 | 205 | .m-r-n-xs { 206 | margin-right: -5px; 207 | } 208 | 209 | .m-r-n-sm { 210 | margin-right: -10px; 211 | } 212 | 213 | .m-r-n { 214 | margin-right: -15px; 215 | } 216 | 217 | .m-r-n-md { 218 | margin-right: -20px; 219 | } 220 | 221 | .m-r-n-lg { 222 | margin-right: -30px; 223 | } 224 | 225 | .m-r-n-xl { 226 | margin-right: -40px; 227 | } 228 | 229 | .m-r-n-xxl { 230 | margin-right: -50px; 231 | } 232 | 233 | .m-b-none { 234 | margin-bottom: 0; 235 | } 236 | 237 | .m-b-xxs { 238 | margin-bottom: 1px; 239 | } 240 | 241 | .m-b-xs { 242 | margin-bottom: 5px; 243 | } 244 | 245 | .m-b-sm { 246 | margin-bottom: 10px; 247 | } 248 | 249 | .m-b { 250 | margin-bottom: 15px; 251 | } 252 | 253 | .m-b-md { 254 | margin-bottom: 20px; 255 | } 256 | 257 | .m-b-lg { 258 | margin-bottom: 30px; 259 | } 260 | 261 | .m-b-xl { 262 | margin-bottom: 40px; 263 | } 264 | 265 | .m-b-xxl { 266 | margin-bottom: 50px; 267 | } 268 | 269 | .m-b-xxxl { 270 | margin-bottom: 70px; 271 | } 272 | 273 | .m-b-n-xxs { 274 | margin-bottom: -1px; 275 | } 276 | 277 | .m-b-n-xs { 278 | margin-bottom: -5px; 279 | } 280 | 281 | .m-b-n-sm { 282 | margin-bottom: -10px; 283 | } 284 | 285 | .m-b-n { 286 | margin-bottom: -15px; 287 | } 288 | 289 | .m-b-n-md { 290 | margin-bottom: -20px; 291 | } 292 | 293 | .m-b-n-lg { 294 | margin-bottom: -30px; 295 | } 296 | 297 | .m-b-n-xl { 298 | margin-bottom: -40px; 299 | } 300 | 301 | .m-b-n-xxl { 302 | margin-bottom: -50px; 303 | } -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/utils.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Headers, 3 | URLSearchParams, 4 | RequestOptions, 5 | Request, 6 | Response 7 | } from "@angular/http"; 8 | import { Observable } from "rxjs/Observable"; 9 | import { HttpService } from "./http.service"; 10 | 11 | export function methodBuilder(method: number) { 12 | return function(url: string) { 13 | return function(target: HttpService, propertyKey: string, descriptor: any) { 14 | 15 | var pPath = target[`${propertyKey}_Path_parameters`], 16 | pQuery = target[`${propertyKey}_Query_parameters`], 17 | pBody = target[`${propertyKey}_Body_parameters`], 18 | pHeader = target[`${propertyKey}_Header_parameters`]; 19 | 20 | descriptor.value = function(...args: any[]) { 21 | var body: string = createBody(pBody, descriptor, args); 22 | var resUrl: string = createPath(url, pPath, args); 23 | var search: URLSearchParams = createQuery(pQuery, args); 24 | var headers: Headers = createHeaders(pHeader, descriptor, this.getDefaultHeaders(), args); 25 | 26 | // Request options 27 | var options = new RequestOptions({ 28 | method, 29 | url: this.getBaseUrl() + resUrl, 30 | headers, 31 | body, 32 | search 33 | }); 34 | 35 | var req = new Request(options); 36 | 37 | // intercept the request 38 | this.requestInterceptor(req); 39 | // make the request and store the observable for later transformation 40 | var observable: Observable = this.http.request(req); 41 | 42 | // intercept the response 43 | observable = this.responseInterceptor(observable, descriptor.adapter); 44 | 45 | return observable; 46 | }; 47 | 48 | return descriptor; 49 | }; 50 | }; 51 | } 52 | 53 | export function paramBuilder(paramName: string) { 54 | return function(key: string) { 55 | return function(target: HttpService, propertyKey: string | symbol, parameterIndex: number) { 56 | var metadataKey = `${propertyKey}_${paramName}_parameters`; 57 | var paramObj: any = { 58 | key: key, 59 | parameterIndex: parameterIndex 60 | }; 61 | 62 | if (Array.isArray(target[metadataKey])) target[metadataKey].push(paramObj); 63 | else target[metadataKey] = [paramObj]; 64 | }; 65 | }; 66 | } 67 | 68 | function createBody(pBody: Array, descriptor: any, args: Array): string { 69 | if (descriptor.isFormData) return args[0]; 70 | return pBody ? JSON.stringify(args[pBody[0].parameterIndex]) : null; 71 | } 72 | 73 | function createPath(url: string, pPath: Array, args: Array): string { 74 | var resUrl: string = url; 75 | 76 | if (pPath) { 77 | for (var k in pPath) { 78 | if (pPath.hasOwnProperty(k)) { 79 | resUrl = resUrl.replace("{" + pPath[k].key + "}", args[pPath[k].parameterIndex]); 80 | } 81 | } 82 | } 83 | 84 | return resUrl; 85 | } 86 | 87 | function createQuery(pQuery: any, args: Array): URLSearchParams { 88 | var search = new URLSearchParams(); 89 | 90 | if (pQuery) { 91 | pQuery 92 | .filter(p => args[p.parameterIndex]) // filter out optional parameters 93 | .forEach(p => { 94 | var key = p.key; 95 | var value = args[p.parameterIndex]; 96 | // if the value is a instance of Object, we stringify it 97 | if (value instanceof Object) { 98 | value = JSON.stringify(value); 99 | } 100 | search.set(encodeURIComponent(key), encodeURIComponent(value)); 101 | }); 102 | } 103 | 104 | return search; 105 | } 106 | 107 | function createHeaders(pHeader: any, descriptor: any, defaultHeaders: any, args: Array): Headers { 108 | var headers = new Headers(defaultHeaders); 109 | 110 | // set method specific headers 111 | for (var k in descriptor.headers) { 112 | if (descriptor.headers.hasOwnProperty(k)) { 113 | if (headers.has(k)) headers.delete(k); 114 | headers.append(k, descriptor.headers[k]); 115 | } 116 | } 117 | 118 | // set parameter specific headers 119 | if (pHeader) { 120 | for (var k in pHeader) { 121 | if (pHeader.hasOwnProperty(k)) { 122 | if (headers.has(k)) headers.delete(k); 123 | headers.append(pHeader[k].key, args[pHeader[k].parameterIndex]); 124 | } 125 | } 126 | } 127 | 128 | return headers; 129 | } -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | // Angular core modules 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { 4 | NgModule, 5 | APP_INITIALIZER 6 | } from '@angular/core'; 7 | import { FormsModule } from '@angular/forms'; 8 | import { 9 | HttpModule, 10 | RequestOptions, 11 | XHRBackend, 12 | Http 13 | } from '@angular/http'; 14 | import { Router } from '@angular/router'; 15 | 16 | // Routes 17 | import { AppRoutingModule } from './app-routing.module'; 18 | 19 | // Modules 20 | import { AppComponent } from './app.component'; 21 | import { AuthModule } from './auth/auth.module'; 22 | import { ProductsModule } from './products/products.module'; 23 | import { HttpServiceModule } from './shared/asyncServices/http/http.module'; 24 | import { UtilityModule} from './shared/utility'; 25 | 26 | // Store 27 | import { store } from './shared/store'; 28 | 29 | // Effects 30 | import { AuthEffects } from './shared/store/effects/auth.effect'; 31 | import { ProductsEffects } from './shared/store/effects/products.effect'; 32 | 33 | // Guards 34 | import { AuthGuard } from './shared/guards/auth.guard'; 35 | import { CanDeactivateGuard } from './shared/guards/canDeactivate.guard'; 36 | 37 | // Services 38 | import { ConfigService } from './app-config.service'; 39 | 40 | // Third party libraries 41 | import { StoreModule } from '@ngrx/store'; 42 | import { EffectsModule } from '@ngrx/effects'; 43 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 44 | import { 45 | TranslateModule, 46 | TranslateLoader, 47 | TranslateStaticLoader 48 | } from 'ng2-translate'; 49 | import { TranslateService } from 'ng2-translate'; 50 | import { 51 | SimpleNotificationsModule, 52 | NotificationsService 53 | } from 'angular2-notifications'; 54 | import { NgxDatatableModule } from '@swimlane/ngx-datatable'; 55 | 56 | /** 57 | * Calling functions or calling new is not supported in metadata when using AoT. 58 | * The work-around is to introduce an exported function. 59 | * 60 | * The reason for this limitation is that the AoT compiler needs to generate the code that calls the factory 61 | * and there is no way to import a lambda from a module, you can only import an exported symbol. 62 | */ 63 | 64 | export function configServiceFactory (config: ConfigService) { 65 | return () => config.load() 66 | } 67 | 68 | @NgModule({ 69 | declarations: [ 70 | AppComponent 71 | ], 72 | imports: [ 73 | // Angular core dependencies 74 | BrowserModule, 75 | FormsModule, 76 | HttpModule, 77 | 78 | // Third party modules 79 | TranslateModule.forRoot(), 80 | SimpleNotificationsModule.forRoot(), 81 | NgxDatatableModule, 82 | 83 | // App custom dependencies 84 | HttpServiceModule.forRoot(), 85 | UtilityModule.forRoot(), 86 | 87 | ProductsModule, 88 | AuthModule, 89 | AppRoutingModule, 90 | 91 | /** 92 | * StoreModule.provideStore is imported once in the root module, accepting a reducer 93 | * function or object map of reducer functions. If passed an object of 94 | * store, combineReducers will be run creating your application 95 | * meta-reducer. This returns all providers for an @ngrx/store 96 | * based application. 97 | */ 98 | StoreModule.provideStore(store), 99 | 100 | /** 101 | * Store devtools instrument the store retaining past versions of state 102 | * and recalculating new states. This enables powerful time-travel 103 | * debugging. 104 | * 105 | * To use the debugger, install the Redux Devtools extension for either 106 | * Chrome or Firefox 107 | * 108 | * See: https://github.com/zalmoxisus/redux-devtools-extension 109 | */ 110 | StoreDevtoolsModule.instrumentOnlyWithExtension(), 111 | 112 | /** 113 | * EffectsModule.run() sets up the effects class to be initialized 114 | * immediately when the application starts. 115 | * 116 | * See: https://github.com/ngrx/effects/blob/master/docs/api.md#run 117 | */ 118 | EffectsModule.run(AuthEffects), 119 | EffectsModule.run(ProductsEffects) 120 | ], 121 | providers: [ 122 | AuthGuard, 123 | CanDeactivateGuard, 124 | ConfigService, 125 | { 126 | provide: APP_INITIALIZER, 127 | useFactory: configServiceFactory, 128 | deps: [ConfigService], 129 | multi: true 130 | } 131 | ], 132 | bootstrap: [AppComponent] 133 | }) 134 | export class AppModule { } -------------------------------------------------------------------------------- /src/assets/fonts/fontello-1afa5ccc/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "css_prefix_text": "icon-", 4 | "css_use_suffix": false, 5 | "hinting": true, 6 | "units_per_em": 1000, 7 | "ascent": 850, 8 | "glyphs": [ 9 | { 10 | "uid": "bf882b30900da12fca090d9796bc3030", 11 | "css": "mail", 12 | "code": 59392, 13 | "src": "fontawesome" 14 | }, 15 | { 16 | "uid": "ccc2329632396dc096bb638d4b46fb98", 17 | "css": "mail-alt", 18 | "code": 61664, 19 | "src": "fontawesome" 20 | }, 21 | { 22 | "uid": "8b80d36d4ef43889db10bc1f0dc9a862", 23 | "css": "user", 24 | "code": 59393, 25 | "src": "fontawesome" 26 | }, 27 | { 28 | "uid": "dd492243d64e21dfe16a92452f7861cb", 29 | "css": "th-large", 30 | "code": 59394, 31 | "src": "fontawesome" 32 | }, 33 | { 34 | "uid": "f805bb95d40c7ef2bc51b3d50d4f2e5c", 35 | "css": "th-list", 36 | "code": 59395, 37 | "src": "fontawesome" 38 | }, 39 | { 40 | "uid": "12f4ece88e46abd864e40b35e05b11cd", 41 | "css": "ok", 42 | "code": 59396, 43 | "src": "fontawesome" 44 | }, 45 | { 46 | "uid": "c5fd349cbd3d23e4ade333789c29c729", 47 | "css": "eye", 48 | "code": 59397, 49 | "src": "fontawesome" 50 | }, 51 | { 52 | "uid": "671f29fa10dda08074a4c6a341bb4f39", 53 | "css": "bell-alt", 54 | "code": 61683, 55 | "src": "fontawesome" 56 | }, 57 | { 58 | "uid": "cd21cbfb28ad4d903cede582157f65dc", 59 | "css": "bell", 60 | "code": 59398, 61 | "src": "fontawesome" 62 | }, 63 | { 64 | "uid": "e99461abfef3923546da8d745372c995", 65 | "css": "cog", 66 | "code": 59399, 67 | "src": "fontawesome" 68 | }, 69 | { 70 | "uid": "5bb103cd29de77e0e06a52638527b575", 71 | "css": "wrench", 72 | "code": 59400, 73 | "src": "fontawesome" 74 | }, 75 | { 76 | "uid": "531bc468eecbb8867d822f1c11f1e039", 77 | "css": "calendar", 78 | "code": 59401, 79 | "src": "fontawesome" 80 | }, 81 | { 82 | "uid": "0d20938846444af8deb1920dc85a29fb", 83 | "css": "logout", 84 | "code": 59402, 85 | "src": "fontawesome" 86 | }, 87 | { 88 | "uid": "ccddff8e8670dcd130e3cb55fdfc2fd0", 89 | "css": "down-open", 90 | "code": 59403, 91 | "src": "fontawesome" 92 | }, 93 | { 94 | "uid": "fe6697b391355dec12f3d86d6d490397", 95 | "css": "up-open", 96 | "code": 59404, 97 | "src": "fontawesome" 98 | }, 99 | { 100 | "uid": "399ef63b1e23ab1b761dfbb5591fa4da", 101 | "css": "right-open", 102 | "code": 59405, 103 | "src": "fontawesome" 104 | }, 105 | { 106 | "uid": "d870630ff8f81e6de3958ecaeac532f2", 107 | "css": "left-open", 108 | "code": 59406, 109 | "src": "fontawesome" 110 | }, 111 | { 112 | "uid": "f3f90c8c89795da30f7444634476ea4f", 113 | "css": "angle-left", 114 | "code": 61700, 115 | "src": "fontawesome" 116 | }, 117 | { 118 | "uid": "7bf14281af5633a597f85b061ef1cfb9", 119 | "css": "angle-right", 120 | "code": 61701, 121 | "src": "fontawesome" 122 | }, 123 | { 124 | "uid": "5de9370846a26947e03f63142a3f1c07", 125 | "css": "angle-up", 126 | "code": 61702, 127 | "src": "fontawesome" 128 | }, 129 | { 130 | "uid": "e4dde1992f787163e2e2b534b8c8067d", 131 | "css": "angle-down", 132 | "code": 61703, 133 | "src": "fontawesome" 134 | }, 135 | { 136 | "uid": "107ce08c7231097c7447d8f4d059b55f", 137 | "css": "ellipsis", 138 | "code": 61761, 139 | "src": "fontawesome" 140 | }, 141 | { 142 | "uid": "3fce1eca43f917c8f23e532749abae5d", 143 | "css": "user-circle-o", 144 | "code": 62142, 145 | "src": "fontawesome" 146 | }, 147 | { 148 | "uid": "9c1376672bb4f1ed616fdd78a23667e9", 149 | "css": "comment-empty", 150 | "code": 61669, 151 | "src": "fontawesome" 152 | }, 153 | { 154 | "uid": "85528017f1e6053b2253785c31047f44", 155 | "css": "comment", 156 | "code": 59407, 157 | "src": "fontawesome" 158 | }, 159 | { 160 | "uid": "266d5d9adf15a61800477a5acf9a4462", 161 | "css": "chart-bar", 162 | "code": 59408, 163 | "src": "fontawesome" 164 | }, 165 | { 166 | "uid": "b86df50a2d898bfcd371fa86c0b8b2fb", 167 | "css": "user-o", 168 | "code": 62144, 169 | "src": "fontawesome" 170 | }, 171 | { 172 | "uid": "cc05df515bebe11df3ada0a5910a8f6d", 173 | "css": "box", 174 | "code": 59409, 175 | "src": "entypo" 176 | } 177 | ] 178 | } -------------------------------------------------------------------------------- /src/app/shared/asyncServices/http/httpResponseHandler.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { TranslateService } from 'ng2-translate'; 3 | import { NotificationsService } from 'angular2-notifications'; 4 | import { ConfigService } from '../../../app-config.service'; 5 | import { Router } from '@angular/router'; 6 | import { Observable } from 'rxjs/Observable'; 7 | 8 | @Injectable() 9 | export class HttpResponseHandler { 10 | constructor( 11 | private router: Router, 12 | private translateService: TranslateService, 13 | private notificationsService: NotificationsService, 14 | private configService: ConfigService 15 | ) {} 16 | 17 | /** 18 | * Global http error handler. 19 | * 20 | * @param error 21 | * @param source 22 | * @returns {ErrorObservable} 23 | */ 24 | public onCatch(response: any, source: Observable): Observable { 25 | switch (response.status) { 26 | case 400: 27 | this.handleBadRequest(response); 28 | break; 29 | 30 | case 401: 31 | this.handleUnauthorized(response); 32 | break; 33 | 34 | case 403: 35 | this.handleForbidden(); 36 | break; 37 | 38 | case 404: 39 | this.handleNotFound(response); 40 | break; 41 | 42 | case 500: 43 | this.handleServerError(); 44 | break; 45 | 46 | default: 47 | break; 48 | } 49 | 50 | return Observable.throw(response); 51 | } 52 | 53 | /** 54 | * Shows notification errors when server response status is 401 55 | * 56 | * @param error 57 | */ 58 | private handleBadRequest(responseBody: any): void { 59 | if (responseBody._body) { 60 | try { 61 | var bodyParsed = responseBody.json(); 62 | this.handleErrorMessages(bodyParsed); 63 | } catch (error) { 64 | this.handleServerError(); 65 | } 66 | } 67 | else this.handleServerError(); 68 | } 69 | 70 | /** 71 | * Shows notification errors when server response status is 401 and redirects user to login page 72 | * 73 | * @param responseBody 74 | */ 75 | private handleUnauthorized(responseBody: any): void { 76 | // Read configuration in order to see if we need to display 401 notification message 77 | let unauthorizedEndpoints: Array = this.configService.get('notifications').unauthorizedEndpoints; 78 | 79 | unauthorizedEndpoints = unauthorizedEndpoints.filter(endpoint => this.getRelativeUrl(responseBody.url) === endpoint); 80 | this.router.navigate(['/login']); 81 | 82 | if (unauthorizedEndpoints.length) { 83 | this.notificationsService.info('Info', this.translateService.instant('ServerError401'), this.configService.get('notifications').options); 84 | } 85 | } 86 | 87 | /** 88 | * Shows notification errors when server response status is 403 89 | */ 90 | private handleForbidden(): void { 91 | this.notificationsService.error('error', this.translateService.instant('ServerError403'), this.configService.get('notifications').options); 92 | this.router.navigate(['/login']); 93 | } 94 | 95 | /** 96 | * Shows notification errors when server response status is 404 97 | * 98 | * @param responseBody 99 | */ 100 | private handleNotFound(responseBody: any): void { 101 | // Read configuration in order to see if we need to display 401 notification message 102 | let notFoundEndpoints: Array = this.configService.get('notifications').notFoundEndpoints; 103 | notFoundEndpoints = notFoundEndpoints.filter(endpoint => this.getRelativeUrl(responseBody.url) === endpoint); 104 | 105 | if (notFoundEndpoints.length) { 106 | let message = this.translateService.instant('ServerError404'), 107 | title = this.translateService.instant('ErrorNotificationTitle'); 108 | 109 | this.showNotificationError(title, message); 110 | } 111 | } 112 | 113 | /** 114 | * Shows notification errors when server response status is 500 115 | */ 116 | private handleServerError(): void { 117 | let message = this.translateService.instant('ServerError500'), 118 | title = this.translateService.instant('ErrorNotificationTitle'); 119 | 120 | this.showNotificationError(title, message); 121 | } 122 | 123 | /** 124 | * Parses server response and shows notification errors with translated messages 125 | * 126 | * @param response 127 | */ 128 | private handleErrorMessages(response: any): void { 129 | if (!response) return; 130 | 131 | for (const key of Object.keys(response)) { 132 | if (Array.isArray(response[key])) { 133 | response[key].forEach((value) => this.showNotificationError('Error', this.getTranslatedValue(value))); 134 | } 135 | else this.showNotificationError('Error', this.getTranslatedValue(response[key])); 136 | } 137 | } 138 | 139 | /** 140 | * Extracts and returns translated value from server response 141 | * 142 | * @param value 143 | * @returns {string} 144 | */ 145 | private getTranslatedValue(value: string): string { 146 | if (value.indexOf('[') > -1) { 147 | let key = value.substring(value.lastIndexOf("[")+1,value.lastIndexOf("]")); 148 | value = this.translateService.instant(key); 149 | } 150 | 151 | return value; 152 | } 153 | 154 | /** 155 | * Returns relative url from the absolute path 156 | * 157 | * @param responseBody 158 | * @returns {string} 159 | */ 160 | private getRelativeUrl(url: string): string { 161 | return url.toLowerCase().replace(/^(?:\/\/|[^\/]+)*\//, ""); 162 | } 163 | 164 | /** 165 | * Shows error notification with given title and message 166 | * 167 | * @param title 168 | * @param message 169 | */ 170 | private showNotificationError(title: string, message: string): void { 171 | this.notificationsService.error(title, message, this.configService.get('notifications').options); 172 | } 173 | } -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | /* **************************************** 3 | * Normalize css 4 | * ****************************************/ 5 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0;}a:focus{outline:thin dotted}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:10px;font-size:0.625rem}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0} 6 | 7 | 8 | /* **************************************** 9 | * Imports 10 | * ****************************************/ 11 | @import "styles/variables"; 12 | @import "styles/paddings"; 13 | @import "styles/margins"; 14 | @import "styles/icons"; 15 | 16 | 17 | /* **************************************** 18 | * Font Families 19 | * ****************************************/ 20 | @font-face { 21 | font-family: 'OpenSans'; 22 | src: url('./assets/fonts/open_sans/OpenSans-Regular.ttf') format('truetype'); 23 | font-weight: 400; 24 | font-style: normal; 25 | } 26 | 27 | @font-face { 28 | font-family: 'OpenSans'; 29 | src: url('./assets/fonts/open_sans/OpenSans-Semibold.ttf') format('truetype'); 30 | font-weight: 600; 31 | font-style: normal; 32 | } 33 | 34 | @font-face { 35 | font-family: 'OpenSans'; 36 | src: url('./assets/fonts/open_sans/OpenSans-Bold.ttf') format('truetype'); 37 | font-weight: 700; 38 | font-style: normal; 39 | } 40 | 41 | @font-face { 42 | font-family: 'OpenSans'; 43 | src: url('./assets/fonts/open_sans/OpenSans-ExtraBold.ttf') format('truetype'); 44 | font-weight: 800; 45 | font-style: normal; 46 | } 47 | 48 | @font-face { 49 | font-family: 'OpenSans'; 50 | src: url('./assets/fonts/open_sans/OpenSans-Light.ttf') format('truetype'); 51 | font-weight: 300; 52 | font-style: normal; 53 | } 54 | 55 | @font-face { 56 | font-family: 'fontello'; 57 | src: url('./assets/fonts/fontello/fontello.eot?75036633'); 58 | src: url('./assets/fonts/fontello/fontello.eot?75036633#iefix') format('embedded-opentype'), 59 | url('./assets/fonts/fontello/fontello.woff?75036633') format('woff'), 60 | url('./assets/fonts/fontello/fontello.ttf?75036633') format('truetype'), 61 | url('./assets/fonts/fontello/fontello.svg?75036633#fontello') format('svg'); 62 | font-weight: normal; 63 | font-style: normal; 64 | } 65 | 66 | 67 | /* **************************************** 68 | * Imports 69 | * ****************************************/ 70 | @import '~@swimlane/ngx-datatable/release/index.css'; 71 | @import '~@swimlane/ngx-datatable/release/themes/material.css'; 72 | 73 | 74 | /* **************************************** 75 | * Scrollbar 76 | * ****************************************/ 77 | *::-webkit-scrollbar { 78 | width: 10px; 79 | height: 10px; 80 | } 81 | 82 | *::-webkit-scrollbar-track { 83 | box-shadow: inset 0 0 3px rgba(0,0,0,0.1); 84 | -webkit-box-shadow: inset 0 0 3px rgba(0,0,0,0.1); 85 | } 86 | 87 | *::-webkit-scrollbar-thumb { 88 | background-color: $color-theme-darkviolet; 89 | outline: 1px solid $color-theme-darkviolet; 90 | border-radius: 5px; 91 | } 92 | 93 | 94 | /* **************************************** 95 | * General 96 | * ****************************************/ 97 | * { 98 | font-family:'OpenSans',Arial,Helvetica,sans-serif; 99 | box-sizing: border-box; 100 | } 101 | 102 | body { 103 | background: $color-grey-verylight; 104 | padding-top:4em; 105 | } 106 | 107 | .form-container { 108 | background: $color-white; 109 | padding:3.5em; 110 | width: 450px; 111 | position:fixed; 112 | left:50%; 113 | margin-left: -225px; 114 | } 115 | 116 | button { 117 | padding:1.2em; 118 | cursor:pointer; 119 | margin-bottom:15px; 120 | font-size:1.3em; 121 | } 122 | 123 | .basic-btn { 124 | background: $color-theme-blue; 125 | color: $color-white; 126 | 127 | &:hover { 128 | opacity: 0.8; 129 | } 130 | } 131 | 132 | input.txt { 133 | background: $color-white !important; 134 | padding:1.3em 1em; 135 | font-size:1.3em; 136 | border: 1px solid $color-grey-light; 137 | } 138 | 139 | h2 { 140 | margin: 1.7em 0 .9em 0; 141 | } 142 | 143 | .columns { 144 | margin-bottom: 30px; 145 | } 146 | 147 | p { 148 | margin: 0.2em 0; 149 | } 150 | 151 | .error-wrapper { 152 | margin-bottom: 15px; 153 | } 154 | 155 | .error-label { 156 | display: block; 157 | color: $color-white; 158 | font-weight: 600; 159 | font-size: 16px; 160 | background: $color-redLight; 161 | padding: 10px; 162 | line-height: 1.2; 163 | text-align: center; 164 | margin-bottom: 1px; 165 | } 166 | 167 | .loading { 168 | width: 30px; 169 | height: 30px; 170 | border: 5px solid $color-grey-light; 171 | position: fixed; 172 | left: 50%; 173 | margin-left: -20px; 174 | top: 50%; 175 | margin-top: -20px; 176 | border-radius: 50%; 177 | } 178 | 179 | .loading:after { 180 | content: ''; 181 | position: absolute; 182 | width: 40px; 183 | height: 7px; 184 | background: $color-grey-verylight; 185 | top: 7px; 186 | left: -10px; 187 | animation: spin 1.2s infinite; 188 | } 189 | 190 | .emptyGrid-label { 191 | position: absolute; 192 | top: 350px; 193 | left: 50%; 194 | width: 500px; 195 | text-align: center; 196 | margin-left: -250px; 197 | text-transform: uppercase; 198 | letter-spacing: 1px; 199 | font-weight: 100; 200 | font-size: 26px; 201 | } 202 | 203 | 204 | @keyframes spin { 205 | 100% { 206 | transform: rotate(360deg); 207 | } 208 | } 209 | 210 | @media (max-width: 600px) { 211 | body { 212 | padding-top:1.2em; 213 | } 214 | 215 | .form-container { 216 | padding:1.2em; 217 | width:90%; 218 | margin-left:-45%; 219 | } 220 | 221 | button { 222 | font-size:1em; 223 | } 224 | } 225 | 226 | .datatable-body-cell { 227 | cursor: pointer; 228 | } 229 | 230 | .ngx-datatable.material.striped .datatable-row-odd { 231 | background: $color-theme-lightBlue; 232 | } 233 | 234 | .ngx-datatable .datatable-footer .datatable-pager .pager li a { 235 | border-radius: 50%; 236 | 237 | &:hover { 238 | border-radius: 50% 239 | } 240 | } 241 | 242 | .ngx-datatable.material .datatable-footer .datatable-pager li.active a { 243 | background-color: $color-theme-lightBlue; 244 | font-weight: normal; 245 | } 246 | 247 | .ngx-datatable.material.single-selection .datatable-body-row.active .datatable-row-group, 248 | .ngx-datatable.material.single-selection .datatable-body-row.active .datatable-row-group:hover, 249 | .ngx-datatable.material.single-selection .datatable-body-row.active, 250 | .ngx-datatable.material.single-selection .datatable-body-row.active:hover { 251 | background-color: $color-theme-darkviolet; 252 | } --------------------------------------------------------------------------------