@ -1,15 +1,86 @@ | |||||
import { Component, OnInit } from '@angular/core'; | |||||
import { Component, OnInit } from '@angular/core'; | |||||
import { CatalogService } from './catalog.service'; | |||||
import { ICatalog } from '../shared/models/catalog.model'; | |||||
import { ICatalogItem } from '../shared/models/catalogItem.model'; | |||||
import { ICatalogType } from '../shared/models/catalogType.model'; | |||||
import { ICatalogBrand } from '../shared/models/catalogBrand.model'; | |||||
import { IPager } from '../shared/models/pager.model'; | |||||
@Component({ | @Component({ | ||||
selector: 'appc-catalog', | |||||
styleUrls: ['./catalog.component.scss'], | |||||
templateUrl: './catalog.component.html' | |||||
selector: 'esh-catalog', | |||||
styleUrls: ['./catalog.component.scss'], | |||||
templateUrl: './catalog.component.html' | |||||
}) | }) | ||||
export class CatalogComponent implements OnInit { | export class CatalogComponent implements OnInit { | ||||
constructor() { } | |||||
brands: ICatalogBrand[]; | |||||
types: ICatalogType[]; | |||||
catalog: ICatalog; | |||||
brandSelected: number; | |||||
typeSelected: number; | |||||
paginationInfo: IPager; | |||||
ngOnInit() { | |||||
console.log('catalog component loaded'); | |||||
} | |||||
constructor(private service: CatalogService) { } | |||||
ngOnInit() { | |||||
this.getBrands(); | |||||
this.getCatalog(10,0); | |||||
this.getTypes(); | |||||
} | |||||
onFilterApplied(event: any) { | |||||
event.preventDefault(); | |||||
this.getCatalog(this.paginationInfo.itemsPage, this.paginationInfo.actualPage, this.brandSelected, this.typeSelected); | |||||
} | |||||
onBrandFilterChanged(event: any, value: number) { | |||||
event.preventDefault(); | |||||
this.brandSelected = value; | |||||
} | |||||
onTypeFilterChanged(event: any, value: number) { | |||||
event.preventDefault(); | |||||
this.typeSelected = value; | |||||
} | |||||
onPageChanged(value: any) { | |||||
console.log('catalog pager event fired' + value); | |||||
event.preventDefault(); | |||||
this.paginationInfo.actualPage = value; | |||||
this.getCatalog(this.paginationInfo.itemsPage, value); | |||||
} | |||||
getCatalog(pageSize:number, pageIndex: number, brand?: number, type?: number) { | |||||
this.service.getCatalog(brand, type).subscribe(catalog => { | |||||
this.catalog = catalog; | |||||
console.log('catalog items retrieved: ' + catalog.count); | |||||
this.paginationInfo = { | |||||
actualPage : catalog.pageIndex, | |||||
itemsPage : catalog.pageSize, | |||||
totalItems : catalog.count, | |||||
totalPages : (catalog.count / catalog.pageSize) | |||||
}; | |||||
console.log(this.paginationInfo); | |||||
}); | |||||
} | |||||
getTypes() { | |||||
this.service.getTypes().subscribe(types => { | |||||
this.types = types; | |||||
let alltypes = { id: null, type: 'All' }; | |||||
this.types.unshift(alltypes); | |||||
console.log('types retrieved: ' + types.length); | |||||
}); | |||||
} | |||||
getBrands() { | |||||
this.service.getBrands().subscribe(brands => { | |||||
this.brands = brands; | |||||
let allBrands = { id: null, brand: 'All' }; | |||||
this.brands.unshift(allBrands); | |||||
console.log('brands retrieved: ' + brands.length); | |||||
}); | |||||
} | |||||
} | } | ||||
@ -1,11 +1,17 @@ | |||||
import { NgModule } from '@angular/core'; | |||||
import { NgModule } from '@angular/core'; | |||||
import { BrowserModule } from '@angular/platform-browser'; | |||||
import { SharedModule } from '../shared/shared.module'; | |||||
import { CatalogComponent } from './catalog.component'; | |||||
import { routing } from './catalog.routes'; | |||||
import { CatalogService } from './catalog.service'; | |||||
import { Pager } from '../shared/components/pager/pager'; | |||||
import { CatalogComponent } from './catalog.component'; | |||||
import { routing } from './catalog.routes'; | |||||
@NgModule({ | @NgModule({ | ||||
imports: [routing], | |||||
declarations: [CatalogComponent] | |||||
imports: [routing, BrowserModule, SharedModule], | |||||
declarations: [CatalogComponent], | |||||
providers: [CatalogService] | |||||
}) | }) | ||||
export class CatalogModule { } | export class CatalogModule { } |
@ -0,0 +1,48 @@ | |||||
import { Injectable } from '@angular/core'; | |||||
import { Response } from '@angular/http'; | |||||
import { DataService } from '../shared/services/data.service'; | |||||
import { ICatalog } from '../shared/models/catalog.model'; | |||||
import { ICatalogBrand } from '../shared/models/catalogBrand.model'; | |||||
import { ICatalogType } from '../shared/models/catalogType.model'; | |||||
import 'rxjs/Rx'; | |||||
import { Observable } from 'rxjs/Observable'; | |||||
import 'rxjs/add/observable/throw'; | |||||
import { Observer } from 'rxjs/Observer'; | |||||
import 'rxjs/add/operator/map'; | |||||
@Injectable() | |||||
export class CatalogService { | |||||
private catalogUrl: string = 'http://eshopcontainers:5101/api/v1/catalog/items'; | |||||
private brandUrl: string = 'http://eshopcontainers:5101/api/v1/catalog/catalogbrands'; | |||||
private typesUrl: string = 'http://eshopcontainers:5101/api/v1/catalog/catalogtypes'; | |||||
constructor(private service: DataService) { | |||||
} | |||||
getCatalog(pageIndex: number, pageSize: number, brand: number, type: number): Observable<ICatalog> { | |||||
var url = this.catalogUrl; | |||||
if (brand || type) { | |||||
url = this.catalogUrl + '/type/' + ((type) ? type.toString() : 'null') + '/brand/' + ((brand) ? brand.toString() : 'null'); | |||||
} | |||||
url = url + '?pageIndex=' + pageIndex + '&pageSize=' + pageSize; | |||||
return this.service.get(url).map((response: Response) => { | |||||
return response.json(); | |||||
}); | |||||
} | |||||
getBrands(): Observable<ICatalogBrand[]> { | |||||
return this.service.get(this.brandUrl).map((response: Response) => { | |||||
return response.json(); | |||||
}); | |||||
} | |||||
getTypes(): Observable<ICatalogType[]> { | |||||
return this.service.get(this.typesUrl).map((response: Response) => { | |||||
return response.json(); | |||||
}); | |||||
}; | |||||
} |
@ -0,0 +1,29 @@ | |||||
<div class="row"> | |||||
<div class="col-xs-4"> | |||||
<nav> | |||||
<ul> | |||||
<li class="page-item"> | |||||
<span class="text previous" id="Previous" | |||||
(click)="onPreviousCliked($event)" | |||||
aria-label="Previous"> | |||||
Previous | |||||
</span> | |||||
</li> | |||||
</ul> | |||||
</nav> | |||||
</div> | |||||
<div class="col-xs-4 u-align-center"><span>Showing {{model?.itemsPage}} of {{model?.totalItems}} products - Page {{model?.actualPage}} - {{model?.totalPages}}</span></div> | |||||
<div class="col-xs-4"> | |||||
<nav> | |||||
<ul> | |||||
<li class="page-item"> | |||||
<span class="text next" id="Next" | |||||
(click)="onNextClicked($event)" | |||||
aria-label="Next"> | |||||
Next | |||||
</span> | |||||
</li> | |||||
</ul> | |||||
</nav> | |||||
</div> | |||||
</div> |
@ -0,0 +1,33 @@ | |||||
import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core'; | |||||
import { IPager } from '../../models/pager.model'; | |||||
@Component({ | |||||
selector: 'esh-pager', | |||||
templateUrl: './pager.html', | |||||
styleUrls: ['./pager.scss'] | |||||
}) | |||||
export class Pager implements OnInit { | |||||
@Output() | |||||
changed: EventEmitter<number> = new EventEmitter<number>(); | |||||
@Input() | |||||
model: IPager; | |||||
ngOnInit() { | |||||
console.log(this.model); | |||||
} | |||||
onNextClicked(event: any) { | |||||
event.preventDefault(); | |||||
console.log('Pager Next Clicked'); | |||||
this.changed.emit(this.model.actualPage + 1); | |||||
} | |||||
onPreviousCliked(event: any) { | |||||
event.preventDefault(); | |||||
this.changed.emit(this.model.actualPage - 1); | |||||
} | |||||
} |
@ -0,0 +1,8 @@ | |||||
import {ICatalogItem} from './catalogItem.model'; | |||||
export interface ICatalog { | |||||
pageIndex: number | |||||
data: ICatalogItem[] | |||||
pageSize: number | |||||
count: number | |||||
} |
@ -0,0 +1,4 @@ | |||||
export interface ICatalogBrand { | |||||
id: number | |||||
brand: string | |||||
} |
@ -0,0 +1,11 @@ | |||||
export interface ICatalogItem { | |||||
id: string; | |||||
name: string; | |||||
description: string; | |||||
price: number; | |||||
pictureUri: string; | |||||
catalogBrandId: number; | |||||
catalogBrand: string; | |||||
catalogTypeId: number; | |||||
catalogType: string; | |||||
} |
@ -0,0 +1,4 @@ | |||||
export interface ICatalogType { | |||||
id: number | |||||
type: string | |||||
} |
@ -1,3 +0,0 @@ | |||||
export class OperationResult { | |||||
constructor(public succeeded: boolean, public message: string) { } | |||||
} |
@ -0,0 +1,6 @@ | |||||
export interface IPager { | |||||
itemsPage: number, | |||||
totalItems: number, | |||||
actualPage: number, | |||||
totalPages: number | |||||
} |
@ -1,13 +0,0 @@ | |||||
import { User } from './user.model'; | |||||
// todo: I dont think user follows angular style guides | |||||
describe('User Model', () => { | |||||
it('has displayName', () => { | |||||
let userModel: User = {displayName: 'test', roles: ['1']}; | |||||
expect(userModel.displayName).toEqual('test'); | |||||
}); | |||||
it('has displayName', () => { | |||||
let userModel: User = {displayName: 'test', roles: ['admin']}; | |||||
expect(userModel.roles[0]).toEqual('admin'); | |||||
}); | |||||
}); |
@ -1,4 +0,0 @@ | |||||
export class User { | |||||
constructor(public displayName: string, public roles: string[]) { | |||||
} | |||||
} |
@ -1,207 +0,0 @@ | |||||
// CREDIT: | |||||
// The vast majority of this code came right from Ben Nadel's post: | |||||
// http://www.bennadel.com/blog/3047-creating-specialized-http-clients-in-angular-2-beta-8.htm | |||||
// | |||||
// My updates are mostly adapting it for Typescript: | |||||
// 1. Importing required modules | |||||
// 2. Adding type notations | |||||
// 3. Using the 'fat-arrow' syntax to properly scope in-line functions | |||||
// | |||||
import 'rxjs/add/operator/map'; | |||||
import 'rxjs/add/operator/catch'; | |||||
import 'rxjs/add/operator/finally'; | |||||
import { Injectable } from '@angular/core'; | |||||
import { Http, Response, RequestOptions, RequestMethod, URLSearchParams } from '@angular/http'; | |||||
import { Observable } from 'rxjs/Observable'; | |||||
import { Subject } from 'rxjs/Subject'; | |||||
import { HttpErrorHandlerService } from './http-error-handler.service'; | |||||
// Import the rxjs operators we need (in a production app you'll | |||||
// probably want to import only the operators you actually use) | |||||
// | |||||
export class ApiGatewayOptions { | |||||
method: RequestMethod; | |||||
url: string; | |||||
headers: any = {}; | |||||
params = {}; | |||||
data = {}; | |||||
} | |||||
@Injectable() | |||||
export class ApiGatewayService { | |||||
// Define the internal Subject we'll use to push the command count | |||||
private pendingCommandsSubject = new Subject<number>(); | |||||
private pendingCommandCount = 0; | |||||
// Provide the *public* Observable that clients can subscribe to | |||||
private pendingCommands$: Observable<number>; | |||||
constructor(private http: Http, private httpErrorHandler: HttpErrorHandlerService) { | |||||
this.pendingCommands$ = this.pendingCommandsSubject.asObservable(); | |||||
} | |||||
// I perform a GET request to the API, appending the given params | |||||
// as URL search parameters. Returns a stream. | |||||
get(url: string, params: any): Observable<Response> { | |||||
let options = new ApiGatewayOptions(); | |||||
options.method = RequestMethod.Get; | |||||
options.url = url; | |||||
options.params = params; | |||||
return this.request(options); | |||||
} | |||||
// I perform a POST request to the API. If both the params and data | |||||
// are present, the params will be appended as URL search parameters | |||||
// and the data will be serialized as a JSON payload. If only the | |||||
// data is present, it will be serialized as a JSON payload. Returns | |||||
// a stream. | |||||
post(url: string, data: any, params: any): Observable<Response> { | |||||
if (!data) { | |||||
data = params; | |||||
params = {}; | |||||
} | |||||
let options = new ApiGatewayOptions(); | |||||
options.method = RequestMethod.Post; | |||||
options.url = url; | |||||
options.params = params; | |||||
options.data = data; | |||||
return this.request(options); | |||||
} | |||||
private request(options: ApiGatewayOptions): Observable<any> { | |||||
options.method = (options.method || RequestMethod.Get); | |||||
options.url = (options.url || ''); | |||||
options.headers = (options.headers || {}); | |||||
options.params = (options.params || {}); | |||||
options.data = (options.data || {}); | |||||
this.interpolateUrl(options); | |||||
this.addXsrfToken(options); | |||||
this.addContentType(options); | |||||
// TODO add auth token when available | |||||
// this.addAuthToken(options); | |||||
let requestOptions = new RequestOptions(); | |||||
requestOptions.method = options.method; | |||||
requestOptions.url = options.url; | |||||
requestOptions.headers = options.headers; | |||||
requestOptions.search = this.buildUrlSearchParams(options.params); | |||||
requestOptions.body = JSON.stringify(options.data); | |||||
let isCommand = (options.method !== RequestMethod.Get); | |||||
if (isCommand) { | |||||
this.pendingCommandsSubject.next(++this.pendingCommandCount); | |||||
} | |||||
let stream = this.http.request(options.url, requestOptions) | |||||
.catch((error: any) => { | |||||
this.httpErrorHandler.handle(error); | |||||
return Observable.throw(error); | |||||
}) | |||||
.map(this.unwrapHttpValue) | |||||
.catch((error: any) => { | |||||
return Observable.throw(this.unwrapHttpError(error)); | |||||
}) | |||||
.finally(() => { | |||||
if (isCommand) { | |||||
this.pendingCommandsSubject.next(--this.pendingCommandCount); | |||||
} | |||||
}); | |||||
return stream; | |||||
} | |||||
private addContentType(options: ApiGatewayOptions): ApiGatewayOptions { | |||||
if (options.method !== RequestMethod.Get) { | |||||
options.headers['Content-Type'] = 'application/json; charset=UTF-8'; | |||||
} | |||||
return options; | |||||
} | |||||
private addAuthToken(options: ApiGatewayOptions): ApiGatewayOptions { | |||||
options.headers.Authorization = 'Bearer ' + JSON.parse(sessionStorage.getItem('accessToken')); | |||||
return options; | |||||
} | |||||
private extractValue(collection: any, key: string): any { | |||||
let value = collection[key]; | |||||
delete (collection[key]); | |||||
return value; | |||||
} | |||||
private addXsrfToken(options: ApiGatewayOptions): ApiGatewayOptions { | |||||
let xsrfToken = this.getXsrfCookie(); | |||||
if (xsrfToken) { | |||||
options.headers['X-XSRF-TOKEN'] = xsrfToken; | |||||
} | |||||
return options; | |||||
} | |||||
private getXsrfCookie(): string { | |||||
let matches = document.cookie.match(/\bXSRF-TOKEN=([^\s;]+)/); | |||||
try { | |||||
return (matches && decodeURIComponent(matches[1])); | |||||
} catch (decodeError) { | |||||
return (''); | |||||
} | |||||
} | |||||
private addCors(options: ApiGatewayOptions): ApiGatewayOptions { | |||||
options.headers['Access-Control-Allow-Origin'] = '*'; | |||||
return options; | |||||
} | |||||
private buildUrlSearchParams(params: any): URLSearchParams { | |||||
let searchParams = new URLSearchParams(); | |||||
for (let key in params) { | |||||
if (params.hasOwnProperty(key)) { | |||||
searchParams.append(key, params[key]); | |||||
} | |||||
} | |||||
return searchParams; | |||||
} | |||||
private interpolateUrl(options: ApiGatewayOptions): ApiGatewayOptions { | |||||
options.url = options.url.replace(/:([a-zA-Z]+[\w-]*)/g, ($0, token) => { | |||||
// Try to move matching token from the params collection. | |||||
if (options.params.hasOwnProperty(token)) { | |||||
return (this.extractValue(options.params, token)); | |||||
} | |||||
// Try to move matching token from the data collection. | |||||
if (options.data.hasOwnProperty(token)) { | |||||
return (this.extractValue(options.data, token)); | |||||
} | |||||
// If a matching value couldn't be found, just replace | |||||
// the token with the empty string. | |||||
return (''); | |||||
}); | |||||
// Clean up any repeating slashes. | |||||
options.url = options.url.replace(/\/{2,}/g, '/'); | |||||
// Clean up any trailing slashes. | |||||
options.url = options.url.replace(/\/+$/g, ''); | |||||
return options; | |||||
} | |||||
private unwrapHttpError(error: any): any { | |||||
try { | |||||
return (error.json()); | |||||
} catch (jsonError) { | |||||
return ({ | |||||
code: -1, | |||||
message: 'An unexpected error occurred.' | |||||
}); | |||||
} | |||||
} | |||||
private unwrapHttpValue(value: Response): any { | |||||
return (value.json()); | |||||
} | |||||
} |
@ -1,23 +0,0 @@ | |||||
import { Injectable } from '@angular/core'; | |||||
import { Observable } from 'rxjs/Rx'; | |||||
import { TranslateLoader } from 'ng2-translate/ng2-translate'; | |||||
import { MissingTranslationHandler, MissingTranslationHandlerParams } from 'ng2-translate/ng2-translate'; | |||||
import { ContentService } from './content.service'; | |||||
@Injectable() | |||||
export class ApiTranslationLoader implements TranslateLoader { | |||||
constructor(private cs: ContentService) { } | |||||
getTranslation(lang: string): Observable<any> { | |||||
return this.cs.get(lang); | |||||
} | |||||
} | |||||
@Injectable() | |||||
export class CustomMissingTranslationHandler implements MissingTranslationHandler { | |||||
handle(params: MissingTranslationHandlerParams) { | |||||
return params.key; | |||||
} | |||||
} |
@ -1,37 +0,0 @@ | |||||
import { Injectable } from '@angular/core'; | |||||
import { Router } from '@angular/router'; | |||||
import { DataService } from './data.service'; | |||||
import { User } from '../models/user.model'; | |||||
@Injectable() | |||||
export class AuthService { | |||||
constructor(private router: Router) { } | |||||
logout() { | |||||
sessionStorage.clear(); | |||||
this.router.navigate(['/login']); | |||||
} | |||||
isLoggedIn(): boolean { | |||||
return this.user(undefined) !== undefined; | |||||
} | |||||
user(user: User): User { | |||||
if (user) { | |||||
sessionStorage.setItem('user', JSON.stringify(user)); | |||||
} | |||||
let userData = JSON.parse(sessionStorage.getItem('user')); | |||||
if (userData) { | |||||
user = new User(userData.displayName, userData.roles); | |||||
} | |||||
return user ? user : undefined; | |||||
} | |||||
setAuth(res: any): void { | |||||
if (res && res.user) { | |||||
sessionStorage.setItem('user', JSON.stringify(res.user)); | |||||
} | |||||
} | |||||
} |
@ -1,13 +0,0 @@ | |||||
import { Injectable } from '@angular/core'; | |||||
import { DataService } from './data.service'; | |||||
@Injectable() | |||||
export class ContentService { | |||||
constructor(public dataService: DataService) { } | |||||
get(lang?: string): any { | |||||
return this.dataService.get('api/content?lang=' + (lang ? lang : 'en')); | |||||
} | |||||
} |
@ -1,17 +1,49 @@ | |||||
import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||
import { Http, Response, RequestOptionsArgs, RequestMethod, Headers } from '@angular/http'; | |||||
import { ApiGatewayService } from './api-gateway.service'; | |||||
import 'rxjs/Rx'; | |||||
import { Observable } from 'rxjs/Observable'; | |||||
import 'rxjs/add/observable/throw'; | |||||
import { Observer } from 'rxjs/Observer'; | |||||
import 'rxjs/add/operator/map'; | |||||
import 'rxjs/add/operator/catch'; | |||||
@Injectable() | @Injectable() | ||||
export class DataService { | export class DataService { | ||||
constructor(public http: ApiGatewayService) { } | |||||
constructor(private http: Http) { } | |||||
get(url: string, params?: any) { | |||||
return this.http.get(url, undefined); | |||||
get(url: string, params?: any): Observable<Response> { | |||||
let options: RequestOptionsArgs = {}; | |||||
options.headers = new Headers(); | |||||
this.addCors(options); | |||||
return this.http.get(url, options).map( | |||||
(res: Response) => { | |||||
return res; | |||||
}).catch(this.handleError); | |||||
} | } | ||||
post(url: string, data: any, params?: any) { | post(url: string, data: any, params?: any) { | ||||
return this.http.post(url, data, params); | return this.http.post(url, data, params); | ||||
} | } | ||||
private addCors(options: RequestOptionsArgs): RequestOptionsArgs { | |||||
options.headers.append('Access-Control-Allow-Origin', '*'); | |||||
return options; | |||||
} | |||||
private handleError(error: any) { | |||||
console.error('server error:', error); | |||||
if (error instanceof Response) { | |||||
let errMessage = ''; | |||||
try { | |||||
errMessage = error.json().error; | |||||
} catch (err) { | |||||
errMessage = error.statusText; | |||||
} | |||||
return Observable.throw(errMessage); | |||||
} | |||||
return Observable.throw(error || 'server error'); | |||||
} | |||||
} | } |
@ -1,25 +0,0 @@ | |||||
// CREDIT: | |||||
// The vast majority of this code came right from Ben Nadel's post: | |||||
// http://www.bennadel.com/blog/3047-creating-specialized-http-clients-in-angular-2-beta-8.htm | |||||
// | |||||
// My updates are mostly adapting it for Typescript: | |||||
// 1. Importing required modules | |||||
// 2. Adding type notations | |||||
// 3. Using the 'fat-arrow' syntax to properly scope in-line functions | |||||
// | |||||
import { Injectable } from '@angular/core'; | |||||
import { Router } from '@angular/router'; | |||||
@Injectable() | |||||
export class HttpErrorHandlerService { | |||||
constructor(private _router: Router) { } | |||||
handle(error: any) { | |||||
if (error.status === 401) { | |||||
sessionStorage.clear(); | |||||
// window.location.href = 'login'; | |||||
this._router.navigate(['Login']); | |||||
} | |||||
} | |||||
} |