diff --git a/src/Services/Identity/Identity.API/Configuration/Config.cs b/src/Services/Identity/Identity.API/Configuration/Config.cs index 1f7a20457..4aa12d4ce 100644 --- a/src/Services/Identity/Identity.API/Configuration/Config.cs +++ b/src/Services/Identity/Identity.API/Configuration/Config.cs @@ -50,7 +50,9 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "orders", - "basket" + "basket", + "locations", + "marketing" } }, new Client @@ -74,7 +76,8 @@ namespace Identity.API.Configuration IdentityServerConstants.StandardScopes.OfflineAccess, "orders", "basket", - "locations" + "locations", + "marketing" }, //Allow requesting refresh tokens for long lived API access AllowOfflineAccess = true, diff --git a/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs new file mode 100644 index 000000000..162fd9db4 --- /dev/null +++ b/src/Services/Marketing/Marketing.API/ViewModel/PaginatedItemsViewModel.cs @@ -0,0 +1,24 @@ +namespace Microsoft.eShopOnContainers.Services.Marketing.API.ViewModel +{ + using System.Collections.Generic; + + + public class PaginatedItemsViewModel where TEntity : class + { + public int PageIndex { get; private set; } + + public int PageSize { get; private set; } + + public long Count { get; private set; } + + public IEnumerable Data { get; private set; } + + public PaginatedItemsViewModel(int pageIndex, int pageSize, long count, IEnumerable data) + { + this.PageIndex = pageIndex; + this.PageSize = pageSize; + this.Count = count; + this.Data = data; + } + } +} diff --git a/src/Web/WebSPA/Client/modules/app.module.ts b/src/Web/WebSPA/Client/modules/app.module.ts index f05d466d8..6a04ea953 100644 --- a/src/Web/WebSPA/Client/modules/app.module.ts +++ b/src/Web/WebSPA/Client/modules/app.module.ts @@ -11,6 +11,7 @@ import { SharedModule } from './shared/shared.module'; import { CatalogModule } from './catalog/catalog.module'; import { OrdersModule } from './orders/orders.module'; import { BasketModule } from './basket/basket.module'; +import { CampaignsModule } from './campaigns/campaigns.module'; @NgModule({ declarations: [AppComponent], @@ -22,7 +23,8 @@ import { BasketModule } from './basket/basket.module'; SharedModule.forRoot(), CatalogModule, OrdersModule, - BasketModule + BasketModule, + CampaignsModule ], providers: [ AppService diff --git a/src/Web/WebSPA/Client/modules/app.routes.ts b/src/Web/WebSPA/Client/modules/app.routes.ts index ecda0c3d0..32e712514 100644 --- a/src/Web/WebSPA/Client/modules/app.routes.ts +++ b/src/Web/WebSPA/Client/modules/app.routes.ts @@ -5,6 +5,8 @@ import { CatalogComponent } from './catalog/catalog.component'; import { OrdersComponent } from './orders/orders.component'; import { OrdersDetailComponent } from './orders/orders-detail/orders-detail.component'; import { OrdersNewComponent } from './orders/orders-new/orders-new.component'; +import { CampaignsComponent } from './campaigns/campaigns.component'; +import { CampaignsDetailComponent } from './campaigns/campaigns-detail/campaigns-detail.component'; export const routes: Routes = [ { path: '', redirectTo: 'catalog', pathMatch: 'full' }, @@ -12,7 +14,9 @@ export const routes: Routes = [ { path: 'catalog', component: CatalogComponent }, { path: 'orders', component: OrdersComponent }, { path: 'orders/:id', component: OrdersDetailComponent }, - { path: 'order', component: OrdersNewComponent } + { path: 'order', component: OrdersNewComponent }, + { path: 'campaigns', component: CampaignsComponent }, + { path: 'campaigns/:id', component: CampaignsDetailComponent } ]; export const routing = RouterModule.forRoot(routes); diff --git a/src/Web/WebSPA/Client/modules/campaign/campaign.module.ts b/src/Web/WebSPA/Client/modules/campaign/campaign.module.ts deleted file mode 100644 index 5f282702b..000000000 --- a/src/Web/WebSPA/Client/modules/campaign/campaign.module.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/campaign/campaign.service.ts b/src/Web/WebSPA/Client/modules/campaign/campaign.service.ts deleted file mode 100644 index 273e2b560..000000000 --- a/src/Web/WebSPA/Client/modules/campaign/campaign.service.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Response } from '@angular/http'; - -import { DataService } from '../shared/services/data.service'; -import { IOrder } from '../shared/models/order.model'; -import { IOrderItem } from '../shared/models/orderItem.model'; -import { IOrderDetail } from "../shared/models/order-detail.model"; -import { SecurityService } from '../shared/services/security.service'; -import { ConfigurationService } from '../shared/services/configuration.service'; - -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 CampaignService { - private marketingUrl: string = ''; - - constructor(private service: DataService, private identityService: SecurityService, private configurationService: ConfigurationService) { - if (this.configurationService.isReady) - this.marketingUrl = this.configurationService.serverSettings.marketingUrl; - else - this.configurationService.settingsLoaded$.subscribe(x => this.marketingUrl = this.configurationService.serverSettings.marketingUrl); - - } - - getOrders(): Observable { - let url = this.marketingUrl + '/api/v1/campaigns/'; - - return this.service.get(url).map((response: Response) => { - return response.json(); - }); - } - - getOrder(id: number): Observable { - let url = this.marketingUrl + '/api/v1/campaigns/' + id; - - return this.service.get(url).map((response: Response) => { - return response.json(); - }); - } - - mapOrderAndIdentityInfoNewOrder(): IOrder { - let order = {}; - let basket = this.basketService.basket; - let identityInfo = this.identityService.UserData; - - console.log(basket); - console.log(identityInfo); - - // Identity data mapping: - order.street = identityInfo.address_street; - order.city = identityInfo.address_city; - order.country = identityInfo.address_country; - order.state = identityInfo.address_state; - order.zipcode = identityInfo.address_zip_code; - order.cardexpiration = identityInfo.card_expiration; - order.cardnumber = identityInfo.card_number; - order.cardsecuritynumber = identityInfo.card_security_number; - order.cardtypeid = identityInfo.card_type; - order.cardholdername = identityInfo.card_holder; - order.total = 0; - order.expiration = identityInfo.card_expiration; - - // basket data mapping: - order.orderItems = new Array(); - basket.items.forEach(x => { - let item: IOrderItem = {}; - item.pictureurl = x.pictureUrl; - item.productId = +x.productId; - item.productname = x.productName; - item.unitprice = x.unitPrice; - item.units = x.quantity; - - order.total += (item.unitprice * item.units); - - order.orderItems.push(item); - }); - - order.buyer = basket.buyerId; - - return order; - } - -} - diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.html b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.html new file mode 100644 index 000000000..756f3f94d --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.html @@ -0,0 +1,17 @@ +Back to campaigns +
+
+
+ {{campaign.name}} +
+

{{campaign.name}}

+

{{campaign.description}}

+

+ + From {{campaign.from | date}} Until {{campaign.to | date}} + +

+
+
+
+
diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.scss b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.scss new file mode 100644 index 000000000..7abe3ca15 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.scss @@ -0,0 +1,57 @@ +@import '../../variables'; + +.esh-campaign_detail { + min-height: 80vh; + margin-top: 1rem; + + &-section { + padding: 1rem 0; + + &--right { + text-align: right; + } + } + + &-titles { + padding-bottom: 1rem; + padding-top: 2rem; + } + + &-title { + text-transform: uppercase; + } + + &-items { + &--border { + border-bottom: $border-light solid $color-foreground-bright; + padding: .5rem 0; + + &:last-of-type { + border-color: transparent; + } + } + } + + $item-height: 8rem; + + &-item { + font-size: $font-size-m; + font-weight: $font-weight-semilight; + + &--middle { + line-height: $item-height; + + @media screen and (max-width: $media-screen-s) { + line-height: $font-size-m; + } + } + + &--mark { + color: $color-secondary; + } + } + + &-image { + height: $item-height; + } +} diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.ts b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.ts new file mode 100644 index 000000000..fc9dae9de --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns-detail/campaigns-detail.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { CampaignsService } from '../campaigns.service'; +import { ICampaignItem } from '../../shared/models/campaignItem.model'; +import { ActivatedRoute } from '@angular/router'; + +@Component({ + selector: 'esh-campaigns_detail', + styleUrls: ['./campaigns-detail.component.scss'], + templateUrl: './campaigns-detail.component.html' +}) +export class CampaignsDetailComponent implements OnInit { + public campaign: ICampaignItem = {}; + + constructor(private service: CampaignsService, private route: ActivatedRoute) { } + + ngOnInit() { + this.route.params.subscribe(params => { + let id = +params['id']; // (+) converts string 'id' to a number + this.getCampaign(id); + }); + } + + getCampaign(id: number) { + this.service.getCampaign(id).subscribe(campaign => { + this.campaign = campaign; + console.log('campaign retrieved: ' + campaign.id); + console.log(this.campaign); + }); + } +} \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.html b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.html new file mode 100644 index 000000000..ea841d0bc --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.html @@ -0,0 +1,30 @@ +Back to catalog +
+
+ + +
+
+ +
+

{{item.name}}

+ {{item.name}} + +
+ +
+
+ + +
+
+ THERE ARE NO RESULTS THAT MATCH YOUR SEARCH +
+
+ diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.scss b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.scss new file mode 100644 index 000000000..17d9da105 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.scss @@ -0,0 +1,65 @@ +@import '../variables'; + +.esh-campaign { + $banner-height: 260px; + + &-title { + position: relative; + top: $banner-height / 3.5; + } + + &-items { + margin-top: 1rem; + } + + &-item { + margin-bottom: 1.5rem; + text-align: center; + width: 33%; + display: inline-block; + float: none !important; + + @media screen and (max-width: $media-screen-m) { + width: 50%; + } + + @media screen and (max-width: $media-screen-s) { + width: 100%; + } + } + + &-thumbnail { + max-width: 370px; + width: 100%; + } + + &-button { + background-color: $color-secondary; + border: 0; + color: $color-foreground-brighter; + cursor: pointer; + font-size: $font-size-m; + height: 3rem; + margin-top: 1rem; + transition: all $animation-speed-default; + width: 80%; + + &.is-disabled { + opacity: .5; + pointer-events: none; + } + + &:hover { + background-color: $color-secondary-darker; + transition: all $animation-speed-default; + } + } + + &-name { + font-size: $font-size-m; + font-weight: $font-weight-semilight; + margin-top: .5rem; + text-align: center; + text-transform: uppercase; + } +} diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.ts b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.ts new file mode 100644 index 000000000..c53995d2d --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit } from '@angular/core'; +import { CampaignsService } from './campaigns.service'; +import { ICampaign } from '../shared/models/campaign.model'; +import { IPager } from '../shared/models/pager.model'; +import { ConfigurationService } from '../shared/services/configuration.service'; + +@Component({ + selector: 'esh-campaigns', + styleUrls: ['./campaigns.component.scss'], + templateUrl: './campaigns.component.html' +}) +export class CampaignsComponent implements OnInit { + private interval = null; + paginationInfo: IPager; + campaigns: ICampaign; + + constructor(private service: CampaignsService, private configurationService: ConfigurationService) { } + + ngOnInit() { + if (this.configurationService.isReady) { + this.getCampaigns(9, 0) + } else { + this.configurationService.settingsLoaded$.subscribe(x => { + this.getCampaigns(9, 0); + }); + } + } + + onPageChanged(value: any) { + console.log('campaigns pager event fired' + value); + //event.preventDefault(); + this.paginationInfo.actualPage = value; + this.getCampaigns(this.paginationInfo.itemsPage, value); + } + + getCampaigns(pageSize: number, pageIndex: number) { + this.service.getCampaigns(pageIndex, pageSize).subscribe(campaigns => { + this.campaigns = campaigns; + this.paginationInfo = { + actualPage : campaigns.pageIndex, + itemsPage : campaigns.pageSize, + totalItems : campaigns.count, + totalPages: Math.ceil(campaigns.count / campaigns.pageSize), + items: campaigns.pageSize + }; + }); + } +} + diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns.module.ts b/src/Web/WebSPA/Client/modules/campaigns/campaigns.module.ts new file mode 100644 index 000000000..4da504ed9 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; + +import { SharedModule } from '../shared/shared.module'; +import { CampaignsComponent } from './campaigns.component'; +import { CampaignsDetailComponent } from './campaigns-detail/campaigns-detail.component'; +import { CampaignsService } from './campaigns.service'; +import { Header } from '../shared/components/header/header'; + +@NgModule({ + imports: [BrowserModule, SharedModule], + declarations: [CampaignsComponent, CampaignsDetailComponent], + providers: [CampaignsService] +}) +export class CampaignsModule { } \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/campaigns/campaigns.service.ts b/src/Web/WebSPA/Client/modules/campaigns/campaigns.service.ts new file mode 100644 index 000000000..e2b4d7159 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/campaigns/campaigns.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { Response } from '@angular/http'; + +import { DataService } from '../shared/services/data.service'; +import { ICampaign } from '../shared/models/campaign.model'; +import { ICampaignItem } from '../shared/models/campaignItem.model'; +import { SecurityService } from '../shared/services/security.service'; +import { ConfigurationService } from '../shared/services/configuration.service'; + +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 CampaignsService { + private marketingUrl: string = ''; + private buyerId: string = ''; + constructor(private service: DataService, private identityService: SecurityService, private configurationService: ConfigurationService) { + if (this.identityService.IsAuthorized) { + if (this.identityService.UserData) { + this.buyerId = this.identityService.UserData.sub; + } + } + + if (this.configurationService.isReady) + this.marketingUrl = this.configurationService.serverSettings.marketingUrl; + else + this.configurationService.settingsLoaded$.subscribe(x => this.marketingUrl = this.configurationService.serverSettings.marketingUrl); + + } + + getCampaigns(pageIndex: number, pageSize: number): Observable { + let url = this.marketingUrl + '/api/v1/campaigns/user/' + this.buyerId; + url = url + '?pageIndex=' + pageIndex + '&pageSize=' + pageSize; + + return this.service.get(url).map((response: Response) => { + return response.json(); + }); + } + + getCampaign(id: number): Observable { + let url = this.marketingUrl + '/api/v1/campaigns/' + id; + + return this.service.get(url).map((response: Response) => { + return response.json(); + }); + } +} + diff --git a/src/Web/WebSPA/Client/modules/shared/components/identity/identity.html b/src/Web/WebSPA/Client/modules/shared/components/identity/identity.html index 9dce33adc..961929bfb 100644 --- a/src/Web/WebSPA/Client/modules/shared/components/identity/identity.html +++ b/src/Web/WebSPA/Client/modules/shared/components/identity/identity.html @@ -25,6 +25,13 @@ +
+ +
My campaigns
+ +
+
diff --git a/src/Web/WebSPA/Client/modules/shared/components/identity/identity.scss b/src/Web/WebSPA/Client/modules/shared/components/identity/identity.scss index 45a8cf3e8..df26fa9db 100644 --- a/src/Web/WebSPA/Client/modules/shared/components/identity/identity.scss +++ b/src/Web/WebSPA/Client/modules/shared/components/identity/identity.scss @@ -1,7 +1,7 @@ @import '../../../variables'; .esh-identity { - line-height: 3rem; + line-height: 2.1rem; position: relative; text-align: right; diff --git a/src/Web/WebSPA/Client/modules/shared/models/campaign.model.ts b/src/Web/WebSPA/Client/modules/shared/models/campaign.model.ts new file mode 100644 index 000000000..88aec800e --- /dev/null +++ b/src/Web/WebSPA/Client/modules/shared/models/campaign.model.ts @@ -0,0 +1,9 @@ +import {ICampaignItem} from './campaignItem.model'; + +export interface ICampaign { + data: ICampaignItem[]; + pageIndex: number; + pageSize: number; + count: number; +} + diff --git a/src/Web/WebSPA/Client/modules/shared/models/campaignItem.model.ts b/src/Web/WebSPA/Client/modules/shared/models/campaignItem.model.ts new file mode 100644 index 000000000..70b66b677 --- /dev/null +++ b/src/Web/WebSPA/Client/modules/shared/models/campaignItem.model.ts @@ -0,0 +1,8 @@ +export interface ICampaignItem { + id: number; + name: string; + description: string; + from: Date; + to: Date; + pictureUri: string; +} \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/shared/services/security.service.ts b/src/Web/WebSPA/Client/modules/shared/services/security.service.ts index a0663a366..483ea4ae4 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/security.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/security.service.ts @@ -82,7 +82,7 @@ export class SecurityService { let client_id = 'js'; let redirect_uri = location.origin + '/'; let response_type = 'id_token token'; - let scope = 'openid profile orders basket'; + let scope = 'openid profile orders basket marketing locations'; let nonce = 'N' + Math.random() + '' + Date.now(); let state = Date.now() + '' + Math.random(); diff --git a/test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs b/test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs index ed2b6f2a7..331592a16 100644 --- a/test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs +++ b/test/Services/FunctionalTests/Services/Marketing/MarketingScenarios.cs @@ -11,6 +11,7 @@ using Xunit; using System.Collections.Generic; using Microsoft.eShopOnContainers.Services.Marketing.API.Dto; + using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; public class MarketingScenarios : MarketingScenariosBase { @@ -49,9 +50,9 @@ .GetAsync(CampaignScenariosBase.Get.UserCampaignsByUserId(userId)); responseBody = await UserLocationCampaignResponse.Content.ReadAsStringAsync(); - var userLocationCampaigns = JsonConvert.DeserializeObject>(responseBody); + var userLocationCampaigns = JsonConvert.DeserializeObject>(responseBody); - Assert.True(userLocationCampaigns.Count > 0); + Assert.True(userLocationCampaigns.Data != null); } } }