diff --git a/build-images.ps1 b/build-images.ps1
index a4404c4ed..0324fbc05 100644
--- a/build-images.ps1
+++ b/build-images.ps1
@@ -18,6 +18,18 @@ dotnet restore $webPathToJson
dotnet build $webPathToJson
dotnet publish $webPathToJson -o $webPathToPub
+# *** WebSPA image ***
+$webSPAPathToJson = $scriptPath + "\src\Web\WebSPA\eShopOnContainers.WebSPA\project.json"
+Write-Host "webSPAPathToJson is $webSPAPathToJson" -ForegroundColor Yellow
+$webSPAPathToPub = $scriptPath + "\pub\webSPA"
+Write-Host "webSPAPathToPub is $webSPAPathToPub" -ForegroundColor Yellow
+
+Write-Host "Restore Dependencies just in case as it is needed to run dotnet publish" -ForegroundColor Blue
+dotnet restore $webSPAPathToJson
+dotnet build $webSPAPathToJson
+dotnet publish $webSPAPathToJson -o $webSPAPathToPub
+
+
#*** Catalog service image ***
$catalogPathToJson = $scriptPath + "\src\Services\Catalog\Catalog.API\project.json"
Write-Host "catalogPathToJson is $catalogPathToJson" -ForegroundColor Yellow
@@ -55,4 +67,4 @@ docker build -t eshop/web $webPathToPub
docker build -t eshop/catalog.api $catalogPathToPub
docker build -t eshop/ordering.api $orderingPathToPub
docker build -t eshop/basket.api $basketPathToPub
-
+docker build -t eshop/webspa $webSPAPathToPub
diff --git a/docker-compose.yml b/docker-compose.yml
index a29fdafb7..43e227c6e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,21 @@ services:
- identity.data
- basket.api
+ webspa:
+ image: eshop/webspa
+ build:
+ context: .
+ dockerfile: Dockerfile
+ environment:
+ - CatalogUrl=http://catalog.api
+ - OrderingUrl=http://ordering.api
+ ports:
+ - "5104:80"
+ depends_on:
+ - catalog.api
+ - identity.data
+ - basket.api
+
catalog.api:
image: eshop/catalog.api
environment:
@@ -28,7 +43,7 @@ services:
- catalog.data
catalog.data:
- image: eshop/mssql-server-private-preview
+ image: microsoft/mssql-server-linux
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
@@ -56,7 +71,7 @@ services:
- "5432:1433"
identity.data:
- image: eshop/mssql-server-private-preview
+ image: microsoft/mssql-server-linux
environment:
- SA_PASSWORD=Pass@word
- ACCEPT_EULA=Y
diff --git a/eShopOnContainers.sln b/eShopOnContainers.sln
index fad394fae..5fe74cfe3 100644
--- a/eShopOnContainers.sln
+++ b/eShopOnContainers.sln
@@ -58,6 +58,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared Code", "Shared Code"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{9CC7814B-72A6-465B-A61C-57B512DEE303}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "eShopOnContainers.WebSPA", "src\Web\WebSPA\eShopOnContainers.WebSPA\eShopOnContainers.WebSPA.xproj", "{9842DB3A-1391-48C7-A49C-2FABD0A18AC2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@@ -506,6 +508,54 @@ Global
{C3C1E2CF-B1F7-4654-BBDC-50143DB22E0B}.Release|x86.ActiveCfg = Release|x86
{C3C1E2CF-B1F7-4654-BBDC-50143DB22E0B}.Release|x86.Build.0 = Release|x86
{C3C1E2CF-B1F7-4654-BBDC-50143DB22E0B}.Release|x86.Deploy.0 = Release|x86
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|x64.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Ad-Hoc|x86.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|ARM.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|ARM.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|iPhone.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|x64.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|x64.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|x86.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.AppStore|x86.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|ARM.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|x64.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Debug|x86.Build.0 = Debug|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|ARM.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|ARM.Build.0 = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|iPhone.Build.0 = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|x64.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|x64.Build.0 = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|x86.ActiveCfg = Release|Any CPU
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -533,5 +583,6 @@ Global
{C3C1E2CF-B1F7-4654-BBDC-50143DB22E0B} = {9CC7814B-72A6-465B-A61C-57B512DEE303}
{778289CA-31F7-4464-8C2A-612EE846F8A7} = {F61357CE-1CC2-410E-8776-B16EEBC98EB8}
{9CC7814B-72A6-465B-A61C-57B512DEE303} = {F61357CE-1CC2-410E-8776-B16EEBC98EB8}
+ {9842DB3A-1391-48C7-A49C-2FABD0A18AC2} = {E279BF0F-7F66-4F3A-A3AB-2CDA66C1CD04}
EndGlobalSection
EndGlobal
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/.gitignore b/src/Web/WebSPA/eShopOnContainers.WebSPA/.gitignore
new file mode 100644
index 000000000..280332f79
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/.gitignore
@@ -0,0 +1,223 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+coverage/**/**
+client/**/*.js
+/doc
+client/**/*.js.map
+npm-debug.log*
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+bower_components/
+**/wwwroot/tmp/
+**/wwwroot/*.bundle.map
+**/wwwroot/*.js
+/wwwroot/dist/
+
+
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/.npmignore b/src/Web/WebSPA/eShopOnContainers.WebSPA/.npmignore
new file mode 100644
index 000000000..d410b8bbf
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/.npmignore
@@ -0,0 +1,237 @@
+/Properties/launchSettings.json
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+bin/
+Bin/
+obj/
+Obj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+/wwwroot/dist/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Microsoft Azure ApplicationInsights config file
+ApplicationInsights.config
+
+# Windows Store app package directory
+AppPackages/
+BundleArtifacts/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+
+# FAKE - F# Make
+.fake/
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/custom-typings.d.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/custom-typings.d.ts
new file mode 100644
index 000000000..8e46a4e30
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/custom-typings.d.ts
@@ -0,0 +1,2 @@
+// Extra variables that live on Global that will be replaced by webpack DefinePlugin
+// declare var process: any;
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand.png b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand.png
new file mode 100644
index 000000000..2afd3dccf
Binary files /dev/null and b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand.png differ
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand_dark.png b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand_dark.png
new file mode 100644
index 000000000..44a65364f
Binary files /dev/null and b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/brand_dark.png differ
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner.png b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner.png
new file mode 100644
index 000000000..0f345a385
Binary files /dev/null and b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner.png differ
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner_text.png b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner_text.png
new file mode 100644
index 000000000..47315ef58
Binary files /dev/null and b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/images/main_banner_text.png differ
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/main.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/main.ts
new file mode 100644
index 000000000..ff50b8628
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/main.ts
@@ -0,0 +1,23 @@
+import './polyfills';
+
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+import { enableProdMode } from '@angular/core';
+
+import { AppModule } from './modules/app.module';
+
+if (process.env.ENV === 'Development') {
+ // Development
+} else {
+ // Production
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule);
+
+// Basic hot reloading support. Automatically reloads and restarts the Angular 2 app each time
+// you modify source files. This will not preserve any application state other than the URL.
+declare var module: any;
+
+if (module.hot) {
+ module.hot.accept();
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/_variables.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/_variables.scss
new file mode 100644
index 000000000..fb6bd2413
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/_variables.scss
@@ -0,0 +1,9 @@
+$primary-colour: #00A69C;
+$primary-accent: #83D01B;
+
+$white-colour: #FFFFFF;
+$grey-colour: #E2E2E2;
+$text-colour: #757575;
+
+$grey-box-shadow: 10px 10px 20px #F2F2F2;
+$grey-box-border: 1px solid #DDDDDD;
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html
new file mode 100644
index 000000000..1d2f5908e
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.scss
new file mode 100644
index 000000000..49193a85d
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.scss
@@ -0,0 +1,2 @@
+@import './_variables.scss';
+
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts
new file mode 100644
index 000000000..fcd39952e
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts
@@ -0,0 +1,37 @@
+import { Title } from '@angular/platform-browser';
+import { Component, ViewEncapsulation, OnInit } from '@angular/core';
+import { RouterModule } from '@angular/router';
+import { TranslateService } from 'ng2-translate/ng2-translate';
+
+import { DataService } from './shared/services/data.service';
+
+/*
+ * App Component
+ * Top Level Component
+ */
+
+@Component({
+ selector: 'appc-app',
+ styleUrls: ['./app.component.scss'],
+ templateUrl: './app.component.html'
+})
+export class AppComponent implements OnInit {
+
+
+ constructor(private translate: TranslateService, private titleService: Title) {
+ // this language will be used as a fallback when a translation isn't found in the current language
+ translate.setDefaultLang('en');
+
+ // the lang to use, if the lang isn't available, it will use the current loader to get them
+ translate.use('en');
+ }
+
+ ngOnInit() {
+ this.translate.get('title')
+ .subscribe(title => this.setTitle(title));
+ }
+
+ public setTitle(newTitle: string) {
+ this.titleService.setTitle(newTitle);
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.module.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.module.ts
new file mode 100644
index 000000000..2716bb5a7
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.module.ts
@@ -0,0 +1,29 @@
+import { NgModule, NgModuleFactoryLoader } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+// import { FormsModule } from '@angular/forms';
+import { HttpModule } from '@angular/http';
+import { RouterModule } from '@angular/Router';
+
+import { routing } from './app.routes';
+import { AppService } from './app.service';
+import { AppComponent } from './app.component';
+import { SharedModule } from './shared/shared.module';
+import { CatalogModule } from './catalog/catalog.module';
+
+@NgModule({
+ declarations: [AppComponent],
+ imports: [
+ BrowserModule,
+ routing,
+ // FormsModule,
+ HttpModule,
+ // Only module that app module loads
+ SharedModule.forRoot(),
+ CatalogModule
+ ],
+ providers: [
+ AppService
+ ],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.routes.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.routes.ts
new file mode 100644
index 000000000..ea0087ca7
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.routes.ts
@@ -0,0 +1,15 @@
+import { Routes, RouterModule } from '@angular/router';
+
+export const routes: Routes = [
+ { path: '', redirectTo: 'catalog', pathMatch: 'full' }
+ // Lazy async modules
+ // {
+ // path: 'login', loadChildren: () => new Promise(resolve => {
+ // (require as any).ensure([], (require: any) => {
+ // resolve(require('./+login/login.module').LoginModule);
+ // });
+ // })
+ // }
+];
+
+export const routing = RouterModule.forRoot(routes);
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.service.ts
new file mode 100644
index 000000000..44cfc1653
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.service.ts
@@ -0,0 +1,6 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class AppService {
+ constructor() { }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.html
new file mode 100644
index 000000000..ccfbe5e48
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.scss
new file mode 100644
index 000000000..d12fa6a9f
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.scss
@@ -0,0 +1,55 @@
+@import '../_variables.scss';
+
+.catalog{
+ &-banner {
+ height: 258px;
+ vertical-align:middle;
+
+ &-image {
+ width: 100%;
+ position: absolute;
+ left: 0;
+ height: 258px;
+ }
+
+ &-text {
+ position:relative;
+ top: 75px;
+ }
+ }
+
+ &-filter {
+ height: 65px;
+
+ &-container {
+ position:absolute;
+ width:100%;
+ background-color: $primary-colour;
+ left:0;
+ height: 65px;
+ }
+ }
+
+ &-content{
+ margin-top: 10px;
+
+ &-item {
+ text-align: center;
+
+ &-image{
+
+ }
+
+ &-button {
+ width: 255px;
+ height: 45px;
+ padding: 10px 20px 10px 20px;
+ background-color: $primary-accent;
+ color: white;
+ font-size: 16px;
+ margin: 10px 0;
+ border:none;
+ }
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.ts
new file mode 100644
index 000000000..53ca23abe
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'appc-catalog',
+ styleUrls: ['./catalog.component.scss'],
+ templateUrl: './catalog.component.html'
+})
+export class CatalogComponent implements OnInit {
+ constructor() { }
+
+ ngOnInit() {
+ console.log('catalog component loaded');
+ }
+
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.module.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.module.ts
new file mode 100644
index 000000000..95e541939
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.module.ts
@@ -0,0 +1,11 @@
+import { NgModule } from '@angular/core';
+
+import { CatalogComponent } from './catalog.component';
+import { routing } from './catalog.routes';
+
+
+@NgModule({
+ imports: [routing],
+ declarations: [CatalogComponent]
+})
+export class CatalogModule { }
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.routes.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.routes.ts
new file mode 100644
index 000000000..83e9b3663
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/catalog/catalog.routes.ts
@@ -0,0 +1,9 @@
+import { Routes, RouterModule } from '@angular/router';
+
+import { CatalogComponent } from './catalog.component';
+
+const routes: Routes = [
+ { path: 'catalog', component: CatalogComponent }
+];
+
+export const routing = RouterModule.forChild(routes);
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.html
new file mode 100644
index 000000000..5d2786ccd
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.html
@@ -0,0 +1,3 @@
+404!
+
+Page you are looking for does not exists.
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.spec.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.spec.ts
new file mode 100644
index 000000000..72384fb48
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.spec.ts
@@ -0,0 +1,11 @@
+/* tslint:disable:no-unused-variable */
+
+import { TestBed, async } from '@angular/core/testing';
+import { PageNotFoundComponent } from './page-not-found.component';
+
+describe('Component: PageNotFound', () => {
+ it('should create an instance', () => {
+ let component = new PageNotFoundComponent();
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.ts
new file mode 100644
index 000000000..ff5f9a4b0
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/page-not-found/page-not-found.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'appc-page-not-found',
+ templateUrl: './page-not-found.component.html',
+ styleUrls: ['./page-not-found.component.scss']
+})
+export class PageNotFoundComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit() {
+ }
+
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/page-heading.directive.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/page-heading.directive.ts
new file mode 100644
index 000000000..b0d707f45
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/page-heading.directive.ts
@@ -0,0 +1,10 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'appc-page-heading',
+ template: `{{text}}
`
+})
+export class PageHeadingComponent {
+ @Input() text: string;
+ constructor() { }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.spec.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.spec.ts
new file mode 100644
index 000000000..1dbb9e44d
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.spec.ts
@@ -0,0 +1,41 @@
+import {
+ fakeAsync,
+ tick,
+ TestBed
+} from '@angular/core/testing';
+import { Component } from '@angular/core';
+import { By } from '@angular/platform-browser/src/dom/debug/by';
+
+// Load the implementations that should be tested
+import { XLargeDirective } from './x-large.directive';
+
+describe('x-large directive', () => {
+ // Create a test component to test directives
+ @Component({
+ template: 'Content
'
+ })
+ class TestComponent { }
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ XLargeDirective,
+ TestComponent
+ ]
+ });
+ });
+
+ it('should sent font-size to x-large', fakeAsync(() => {
+ TestBed.compileComponents().then(() => {
+
+ const fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ tick();
+ const element = fixture.debugElement.query(By.css('div'));
+
+ // expect(element.nativeElement.style.fontSize).toBe('x-large');
+
+ });
+ }));
+
+});
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.ts
new file mode 100644
index 000000000..0cf58bcc5
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/directives/x-large.directive.ts
@@ -0,0 +1,18 @@
+import { Directive, ElementRef, Renderer } from '@angular/core';
+/*
+ * Directive
+ * XLarge is a simple directive to show how one is made
+ */
+@Directive({
+ selector: '[appdXlarge]' // using [ ] means selecting attributes
+})
+export class XLargeDirective {
+ constructor(element: ElementRef, renderer: Renderer) {
+ // simple DOM manipulation to set font size to x-large
+ // `nativeElement` is the direct reference to the DOM element
+ // element.nativeElement.style.fontSize = 'x-large';
+
+ // for server/webworker support use the renderer
+ renderer.setElementStyle(element.nativeElement, 'fontSize', 'x-large');
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-base.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-base.ts
new file mode 100644
index 000000000..97ffcf111
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-base.ts
@@ -0,0 +1,36 @@
+export class ControlBase{
+ value: T;
+ key: string;
+ label: string;
+ placeholder: string;
+ required: boolean;
+ minlength: number;
+ maxlength: number;
+ order: number;
+ type: string;
+ class: string;
+
+ constructor(options: {
+ value?: T,
+ key?: string,
+ label?: string,
+ placeholder?: string,
+ required?: boolean,
+ minlength?: number,
+ maxlength?: number,
+ order?: number,
+ type?: string,
+ class?: string;
+ } = {}) {
+ this.value = options.value;
+ this.key = options.key || '';
+ this.label = options.label || '';
+ this.placeholder = options.placeholder || '';
+ this.required = !!options.required;
+ this.minlength = options.minlength;
+ this.maxlength = options.maxlength;
+ this.order = options.order === undefined ? 1 : options.order;
+ this.type = options.type || '';
+ this.class = options.class || '';
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-checkbox.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-checkbox.ts
new file mode 100644
index 000000000..dbff5e20e
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-checkbox.ts
@@ -0,0 +1,11 @@
+import { ControlBase } from './control-base';
+
+export class ControlCheckbox extends ControlBase {
+ type: string;
+
+ constructor(options: any = {}) {
+ super(options);
+ this.type = 'checkbox';
+ this.value = options.value || false;
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-dropdown.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-dropdown.ts
new file mode 100644
index 000000000..793af39af
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-dropdown.ts
@@ -0,0 +1,11 @@
+import { ControlBase } from './control-base';
+
+export class ControlDropdown extends ControlBase {
+ options: { key: string, value: string }[] = [];
+
+ constructor(options: any = {}) {
+ super(options);
+ this.type = 'dropdown';
+ this.options = options.options || [];
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-textbox.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-textbox.ts
new file mode 100644
index 000000000..135cd5432
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/control-textbox.ts
@@ -0,0 +1,8 @@
+import { ControlBase } from './control-base';
+
+export class ControlTextbox extends ControlBase {
+ constructor(options: any = {}) {
+ super(options);
+ this.type = options.type || 'textbox';
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.html
new file mode 100644
index 000000000..dcfe683e2
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.html
@@ -0,0 +1,26 @@
+
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.ts
new file mode 100644
index 000000000..8df976694
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form-control.component.ts
@@ -0,0 +1,26 @@
+import { Component, Input } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+import { ControlBase } from './control-base';
+import { ErrorMessageComponent } from './error-message.component';
+
+@Component({
+ selector: 'appc-dynamic-control',
+ templateUrl: './dynamic-form-control.component.html'
+})
+export class DynamicFormControlComponent {
+ @Input() control;
+ @Input() form;
+
+ constructor() {
+ this.control = undefined;
+ this.form = undefined;
+ }
+
+ get valid() {
+ return this.form.controls[this.control.key].valid;
+ }
+
+ get invalid() {
+ return !this.form.controls[this.control.key].valid && this.form.controls[this.control.key].touched;
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.html
new file mode 100644
index 000000000..a3660cb01
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.ts
new file mode 100644
index 000000000..bf2874d48
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/dynamic-form.component.ts
@@ -0,0 +1,30 @@
+import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+import { ControlBase } from './control-base';
+import { FormControlService } from './form-control.service';
+
+@Component({
+ selector: 'appc-dynamic-form',
+ templateUrl: './dynamic-form.component.html'
+})
+export class DynamicFormComponent implements OnInit {
+
+ @Input() controls: ControlBase[] = [];
+ @Input() btnText: string = 'Submit'; // Default value at least
+ @Input() formClass: string = 'form-horizontal';
+ // Note: don't keep name of output events as same as native events such as submit etc.
+ @Output() formsubmit: EventEmitter = new EventEmitter();
+ form: FormGroup;
+
+ constructor(private _controlService: FormControlService) { }
+
+ ngOnInit() {
+ let sortedControls = this.controls.sort((a, b) => a.order - b.order);
+ this.form = this._controlService.toControlGroup(sortedControls);
+ }
+
+ onSubmit() {
+ this.formsubmit.emit(this.form.value);
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-message.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-message.component.ts
new file mode 100644
index 000000000..ff4a97f08
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-message.component.ts
@@ -0,0 +1,25 @@
+import { Component, Host, Input } from '@angular/core';
+import { FormGroupDirective } from '@angular/forms';
+
+import { ControlBase } from './control-base';
+import { ValidationService } from './validation.service';
+
+@Component({
+ selector: 'appc-control-error-message',
+ template: ` {{errorMessage}}
`
+})
+export class ErrorMessageComponent {
+ @Input() control: ControlBase;
+ @Input() form: FormGroupDirective;
+ constructor() { }
+
+ get errorMessage() {
+ let c = this.form.form.get(this.control.key);
+ for (let propertyName in c.errors) {
+ if (c.errors.hasOwnProperty(propertyName) && c.touched) {
+ return ValidationService.getValidatorErrorMessage(propertyName, this.control.minlength || this.control.maxlength);
+ }
+ }
+ return undefined;
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.html
new file mode 100644
index 000000000..638dd9cb6
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.html
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.ts
new file mode 100644
index 000000000..24d31a2ea
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/error-summary.component.ts
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'appc-error-summary',
+ templateUrl: './error-summary.component.html'
+})
+export class ErrorSummaryComponent {
+ @Input() errors: string | string[];
+
+ constructor() { }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/form-control.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/form-control.service.ts
new file mode 100644
index 000000000..36c8ba92c
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/form-control.service.ts
@@ -0,0 +1,41 @@
+import { Injectable } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+
+import { ControlBase } from './control-base';
+import { ValidationService } from './validation.service';
+
+@Injectable()
+export class FormControlService {
+ constructor() { }
+
+ toControlGroup(controls: ControlBase[]) {
+ let group: any = {};
+
+ controls.forEach(control => {
+ let validators = [];
+ // Required
+ if (control.required) {
+ validators.push(Validators.required);
+ }
+ // Minlength
+ if (control.minlength) {
+ validators.push(Validators.minLength(control.minlength));
+ }
+ // Maxlength
+ if (control.maxlength) {
+ validators.push(Validators.minLength(control.maxlength));
+ }
+ // Email
+ if (control.type === 'email') {
+ validators.push(ValidationService.emailValidator);
+ }
+ // Password
+ if (control.type === 'password') {
+ validators.push(ValidationService.passwordValidator);
+ }
+ group[control.key] = new FormControl(control.value || '', validators);
+ });
+
+ return new FormGroup(group);
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/validation.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/validation.service.ts
new file mode 100644
index 000000000..79c35da7c
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/forms/validation.service.ts
@@ -0,0 +1,42 @@
+export class ValidationService {
+
+ static getValidatorErrorMessage(code: string, fieldLength: number) {
+ let config: any = {
+ 'required': 'This is a required field',
+ 'minlength': 'Minimum length is ' + fieldLength,
+ 'maxlength': 'Maximum length is ' + fieldLength,
+ 'invalidCreditCard': 'Invalid credit card number',
+ 'invalidEmailAddress': 'Invalid email address',
+ 'invalidPassword': 'Password must be at least 6 characters long, and contain a number and special character.'
+ };
+ return config[code];
+ }
+
+ static creditCardValidator(control: any) {
+ // Visa, MasterCard, American Express, Diners Club, Discover, JCB
+ if (control.value.match(/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/)) {
+ return undefined;
+ } else {
+ return { 'invalidCreditCard': true };
+ }
+ }
+
+ static emailValidator(control: any) {
+ // RFC 2822 compliant regex
+ if (control.value.match(/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/)) {
+ return undefined;
+ } else {
+ return { 'invalidEmailAddress': true };
+ }
+ }
+
+ static passwordValidator(control: any) {
+ // {6,100} - Assert password is between 6 and 100 characters
+ // (?=.*[0-9]) - Assert a string has at least one number
+ if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!"@#$%^&*]{6,100}$/)) {
+ return undefined;
+ } else {
+ return { 'invalidPassword': true };
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.html
new file mode 100644
index 000000000..977840161
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.html
@@ -0,0 +1,8 @@
+
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.scss
new file mode 100644
index 000000000..533cfc934
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.scss
@@ -0,0 +1,8 @@
+@import '../../_variables.scss';
+
+.footer {
+ padding-top: 40px;
+ padding-bottom: 40px;
+ margin-top: 40px;
+ border-top: 1px solid #eee;
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.ts
new file mode 100644
index 000000000..5c21dc14b
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/footer.component.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'appc-footer',
+ styleUrls: ['./footer.component.scss'],
+ templateUrl: './footer.component.html'
+})
+export class FooterComponent {
+ constructor() { }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.html
new file mode 100644
index 000000000..ac38e6319
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.html
@@ -0,0 +1,62 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.scss
new file mode 100644
index 000000000..492bb3492
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.scss
@@ -0,0 +1,5 @@
+@import '../../_variables.scss';
+
+.header-brand {
+ background-image:url('../../../images/brand.png')
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.ts
new file mode 100644
index 000000000..6e23eb2c0
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/layout/header.component.ts
@@ -0,0 +1,18 @@
+import { Component, Inject } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AuthService } from '../services/auth.service';
+
+@Component({
+ selector: 'appc-header',
+ styleUrls: ['./header.component.scss'],
+ templateUrl: './header.component.html'
+})
+export class HeaderComponent {
+ isCollapsed: boolean = true;
+ constructor(private router: Router, private authService: AuthService) { }
+
+ toggleNav() {
+ this.isCollapsed = !this.isCollapsed;
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/operation-result.model.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/operation-result.model.ts
new file mode 100644
index 000000000..6a98bfafa
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/operation-result.model.ts
@@ -0,0 +1,3 @@
+export class OperationResult {
+ constructor(public succeeded: boolean, public message: string) { }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.spec.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.spec.ts
new file mode 100644
index 000000000..b38cd5490
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.spec.ts
@@ -0,0 +1,13 @@
+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');
+ });
+});
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.ts
new file mode 100644
index 000000000..7b1dbbc9c
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/user.model.ts
@@ -0,0 +1,4 @@
+export class User {
+ constructor(public displayName: string, public roles: string[]) {
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.spec.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.spec.ts
new file mode 100644
index 000000000..3001dd715
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.spec.ts
@@ -0,0 +1,21 @@
+import { UppercasePipe } from './uppercase.pipe';
+
+describe('Pipe appfUppercase', () => {
+ let pipe: UppercasePipe;
+
+ beforeEach(() => {
+ pipe = new UppercasePipe();
+ });
+
+ it('transforms "abc" to "ABC"', () => {
+ expect(pipe.transform('abc')).toEqual('ABC');
+ });
+
+ it('transforms "abc def" to "ABC DEF"', () => {
+ expect(pipe.transform('abc def')).toEqual('ABC DEF');
+ });
+
+ it('leaves "ABC DEF" unchanged', () => {
+ expect(pipe.transform('ABC DEF')).toEqual('ABC DEF');
+ });
+});
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.ts
new file mode 100644
index 000000000..d7b71d78c
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/pipes/uppercase.pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'appfUppercase'
+})
+export class UppercasePipe implements PipeTransform {
+ transform(value: string) {
+ return value.toUpperCase();
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-gateway.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-gateway.service.ts
new file mode 100644
index 000000000..6df54875e
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-gateway.service.ts
@@ -0,0 +1,207 @@
+// 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();
+ private pendingCommandCount = 0;
+
+ // Provide the *public* Observable that clients can subscribe to
+ private pendingCommands$: Observable;
+
+ 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 {
+ 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 {
+ 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 {
+ 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());
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-translation-loader.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-translation-loader.service.ts
new file mode 100644
index 000000000..2105706b9
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/api-translation-loader.service.ts
@@ -0,0 +1,23 @@
+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 {
+ return this.cs.get(lang);
+ }
+}
+
+@Injectable()
+export class CustomMissingTranslationHandler implements MissingTranslationHandler {
+ handle(params: MissingTranslationHandlerParams) {
+ return params.key;
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/auth.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/auth.service.ts
new file mode 100644
index 000000000..2f3d8ed64
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/auth.service.ts
@@ -0,0 +1,37 @@
+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));
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/content.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/content.service.ts
new file mode 100644
index 000000000..8f67ec197
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/content.service.ts
@@ -0,0 +1,13 @@
+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'));
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts
new file mode 100644
index 000000000..1b08aeada
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts
@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+
+import { ApiGatewayService } from './api-gateway.service';
+
+@Injectable()
+export class DataService {
+
+ constructor(public http: ApiGatewayService) { }
+
+ get(url: string, params?: any) {
+ return this.http.get(url, undefined);
+ }
+
+ post(url: string, data: any, params?: any) {
+ return this.http.post(url, data, params);
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/http-error-handler.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/http-error-handler.service.ts
new file mode 100644
index 000000000..23f4825f1
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/http-error-handler.service.ts
@@ -0,0 +1,25 @@
+// 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']);
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/notification.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/notification.service.ts
new file mode 100644
index 000000000..23ca86ec7
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/notification.service.ts
@@ -0,0 +1,13 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class NotificationService {
+
+ printSuccessMessage(message: string) {
+ console.log(message);
+ }
+
+ printErrorMessage(message: string) {
+ console.error(message);
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/utility.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/utility.service.ts
new file mode 100644
index 000000000..fea7bbd49
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/utility.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Injectable()
+export class UtilityService {
+
+ private _router: Router;
+
+ constructor(router: Router) {
+ this._router = router;
+ }
+
+ convertDateTime(date: Date) {
+ let _formattedDate = new Date(date.toString());
+ return _formattedDate.toDateString();
+ }
+
+ navigate(path: string) {
+ this._router.navigate([path]);
+ }
+
+ navigateToSignIn() {
+ this.navigate('/login');
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts
new file mode 100644
index 000000000..b4e717f36
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts
@@ -0,0 +1,86 @@
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms';
+import { RouterModule } from '@angular/router';
+import { HttpModule, JsonpModule } from '@angular/http';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { TranslateModule, TranslateLoader } from 'ng2-translate/ng2-translate';
+
+import { PageHeadingComponent } from './directives/page-heading.directive';
+import { DynamicFormComponent } from './forms/dynamic-form.component';
+import { DynamicFormControlComponent } from './forms/dynamic-form-control.component';
+import { ErrorMessageComponent } from './forms/error-message.component';
+import { ErrorSummaryComponent } from './forms/error-summary.component';
+import { FormControlService } from './forms/form-control.service';
+
+import { HeaderComponent } from './layout/header.component';
+import { FooterComponent } from './layout/footer.component';
+// Services
+import { DataService } from './services/data.service';
+import { ApiGatewayService } from './services/api-gateway.service';
+import { AuthService } from './services/auth.service';
+import { HttpErrorHandlerService } from './services/http-error-handler.service';
+import { ApiTranslationLoader } from './services/api-translation-loader.service';
+import { ContentService } from './services/content.service';
+import { UtilityService } from './services/utility.service';
+import { UppercasePipe } from './pipes/uppercase.pipe';
+
+@NgModule({
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ RouterModule,
+ NgbModule.forRoot(),
+ // No need to export as these modules don't expose any components/directive etc'
+ HttpModule,
+ JsonpModule,
+ TranslateModule.forRoot({ provide: TranslateLoader, useClass: ApiTranslationLoader })
+ ],
+ declarations: [
+ DynamicFormComponent,
+ DynamicFormControlComponent,
+ ErrorMessageComponent,
+ ErrorSummaryComponent,
+ FooterComponent,
+ HeaderComponent,
+ PageHeadingComponent,
+ UppercasePipe
+ ],
+ exports: [
+ // Modules
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ RouterModule,
+ NgbModule,
+ TranslateModule,
+ // Providers, Components, directive, pipes
+ DynamicFormComponent,
+ DynamicFormControlComponent,
+ ErrorSummaryComponent,
+ ErrorMessageComponent,
+ FooterComponent,
+ HeaderComponent,
+ PageHeadingComponent,
+ UppercasePipe
+ ]
+
+})
+export class SharedModule {
+ static forRoot(): ModuleWithProviders {
+ return {
+ ngModule: SharedModule,
+ providers: [
+ // Providers
+ HttpErrorHandlerService,
+ ApiGatewayService,
+ AuthService,
+ DataService,
+ ContentService,
+ FormControlService,
+ UtilityService
+ ]
+ };
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/polyfills.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/polyfills.ts
new file mode 100644
index 000000000..2cdf1a036
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/polyfills.ts
@@ -0,0 +1,23 @@
+// Added parts of es6 which are necessary for your project or your browser support requirements.
+import 'core-js/es6/symbol';
+import 'core-js/es6/object';
+import 'core-js/es6/function';
+import 'core-js/es6/parse-int';
+import 'core-js/es6/parse-float';
+import 'core-js/es6/number';
+import 'core-js/es6/math';
+import 'core-js/es6/string';
+import 'core-js/es6/date';
+import 'core-js/es6/array';
+import 'core-js/es6/regexp';
+import 'core-js/es6/map';
+import 'core-js/es6/set';
+import 'core-js/es6/weak-map';
+import 'core-js/es6/weak-set';
+import 'core-js/es6/typed';
+import 'core-js/es6/reflect';
+// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
+// import 'core-js/es6/promise';
+
+import 'core-js/es7/reflect';
+import 'zone.js/dist/zone';
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/vendor.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/vendor.ts
new file mode 100644
index 000000000..4f40d6b4a
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/vendor.ts
@@ -0,0 +1,20 @@
+// For vendors for example jQuery, Lodash, angular2-jwt just import them here unless you plan on
+// chunking vendors files for async loading. You would need to import the async loaded vendors
+// at the entry point of the async loaded file. Also see custom-typings.d.ts as you also need to
+// run `typings install x` where `x` is your module
+
+// Angular 2
+import '@angular/platform-browser';
+import '@angular/platform-browser-dynamic';
+import '@angular/core';
+import '@angular/common';
+import '@angular/forms';
+import '@angular/http';
+import '@angular/router';
+
+// RxJS
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/mergeMap';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/finally';
+import 'rxjs/add/observable/throw';
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Dockerfile b/src/Web/WebSPA/eShopOnContainers.WebSPA/Dockerfile
new file mode 100644
index 000000000..aa8183794
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Dockerfile
@@ -0,0 +1,14 @@
+# FROM microsoft/aspnet:1.0.0-rc1-update1
+# COPY . /app
+# WORKDIR /app
+# RUN ["dnu", "restore"]
+# EXPOSE 5104/tcp
+# ENTRYPOINT ["dnx", "-p", "project.json", "web"]
+
+FROM microsoft/aspnetcore:1.0.1
+ENTRYPOINT ["dotnet", "eShopOnContainers.WebSPA.dll"]
+ARG source=.
+WORKDIR /app
+ENV ASPNETCORE_URLS http://*:80
+EXPOSE 80
+COPY $source .
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs b/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs
new file mode 100644
index 000000000..3622da0be
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs
@@ -0,0 +1,27 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+
+namespace eShopConContainers.WebSPA
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var config = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("hosting.json", optional: true)
+ .Build();
+
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseConfiguration(config)
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Properties/launchSettings.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/Properties/launchSettings.json
new file mode 100644
index 000000000..93edb40be
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Properties/launchSettings.json
@@ -0,0 +1,18 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1250/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Server/Controllers/HomeController.cs b/src/Web/WebSPA/eShopOnContainers.WebSPA/Server/Controllers/HomeController.cs
new file mode 100644
index 000000000..c70a67769
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Server/Controllers/HomeController.cs
@@ -0,0 +1,35 @@
+// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
+
+using System.Linq;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+
+namespace eShopConContainers.WebSPA.Server.Controllers
+{
+ public class HomeController : Controller
+ {
+ private readonly IHostingEnvironment _env;
+
+ public HomeController(IHostingEnvironment env)
+ {
+ _env = env;
+ }
+
+ public IActionResult Index()
+ {
+ ViewBag.HashedMain = GetHashedMainDotJs();
+
+ return View();
+ }
+
+ public string GetHashedMainDotJs()
+ {
+ var basePath = _env.WebRootPath + "//dist//";
+ var info = new System.IO.DirectoryInfo(basePath);
+ var file = info.GetFiles().Where(f => f.Name.StartsWith("main.") && !f.Name.EndsWith("bundle.map")).FirstOrDefault();
+
+ return file.Name;
+ }
+
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Startup.cs b/src/Web/WebSPA/eShopOnContainers.WebSPA/Startup.cs
new file mode 100644
index 000000000..705ba34f2
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Startup.cs
@@ -0,0 +1,93 @@
+using System;
+using Microsoft.AspNetCore.Antiforgery;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.SpaServices.Webpack;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Serialization;
+
+namespace eShopConContainers.WebSPA
+{
+ public class Startup
+ {
+ private IHostingEnvironment _hostingEnv;
+ public Startup(IHostingEnvironment env)
+ {
+ _hostingEnv = env;
+
+ var builder = new ConfigurationBuilder()
+ .SetBasePath(env.ContentRootPath)
+ .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
+ .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
+ .AddEnvironmentVariables();
+
+ if (env.IsDevelopment())
+ {
+ // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709
+ builder.AddUserSecrets();
+ }
+
+ Configuration = builder.Build();
+ }
+
+ public static IConfigurationRoot Configuration { get; set; }
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
+
+ services.AddMvc()
+ .AddJsonOptions(options =>
+ {
+ options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
+ });
+ }
+
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+
+ app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
+ {
+ HotModuleReplacement = true,
+ ConfigFile = "config/webpack.config.js"
+ });
+
+ }
+
+ // Configure XSRF middleware, This pattern is for SPA style applications where XSRF token is added on Index page
+ // load and passed back token on every subsequent async request
+ app.Use(async (context, next) =>
+ {
+ if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase))
+ {
+ var tokens = antiforgery.GetAndStoreTokens(context);
+ context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
+ }
+ await next.Invoke();
+ });
+
+ app.UseStaticFiles();
+
+
+ app.UseMvc(routes =>
+ {
+ routes.MapRoute(
+ name: "default",
+ template: "{controller=Home}/{action=Index}/{id?}");
+
+ routes.MapSpaFallbackRoute(
+ name: "spa-fallback",
+ defaults: new { controller = "Home", action = "Index" });
+ });
+ }
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Home/Index.cshtml b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Home/Index.cshtml
new file mode 100644
index 000000000..57d9751b4
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Home/Index.cshtml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Shared/_Layout.cshtml b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Shared/_Layout.cshtml
new file mode 100644
index 000000000..c7d974e27
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/Shared/_Layout.cshtml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ eShopConContainers.WebSPA
+
+
+
+
+
+ @RenderBody()
+
+
+
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewImports.cshtml b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewImports.cshtml
new file mode 100644
index 000000000..1c0d391f9
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewImports.cshtml
@@ -0,0 +1,3 @@
+
+@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
+@addTagHelper "*, Microsoft.AspNetCore.SpaServices"
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewStart.cshtml b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewStart.cshtml
new file mode 100644
index 000000000..820a2f6e0
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "_Layout";
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/appsettings.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/appsettings.json
new file mode 100644
index 000000000..e31f67b1c
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/appsettings.json
@@ -0,0 +1,47 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=AspNetCore.db"
+ },
+ "Logging": {
+ "IncludeScopes": false,
+ "LogLevel": {
+ "Default": "Debug",
+ "System": "Information",
+ "Microsoft": "Information"
+ }
+ },
+ "Email": {
+ "From": "",
+ "Subject": "",
+ "SendGrid": {
+ "Username": "",
+ "Password": ""
+ }
+ },
+ "Authentication": {
+ "Google": {
+ "ClientId": "",
+ "ClientSecret": ""
+ },
+ "Facebook": {
+ "AppId": "",
+ "AppSecret": ""
+ },
+ "Microsoft": {
+ "ClientId": "",
+ "ClientSecret": ""
+ },
+ "Twitter": {
+ "ConsumerKey": "",
+ "ConsumerSecret": ""
+ },
+ "Github": {
+ "ClientId": "",
+ "ClientSecret": ""
+ },
+ "LinkedIn": {
+ "ClientId": "",
+ "ClientSecret": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/config/helpers.js b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/helpers.js
new file mode 100644
index 000000000..9d37e78a8
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/helpers.js
@@ -0,0 +1,23 @@
+/**
+ * @author: @AngularClass
+ */
+
+var path = require('path');
+
+// Helper functions
+var ROOT = path.resolve(__dirname, '..');
+
+console.log('root directory:', root() + '\n');
+
+function hasProcessFlag(flag) {
+ return process.argv.join('').indexOf(flag) > -1;
+}
+
+function root(args) {
+ args = Array.prototype.slice.call(arguments, 0);
+ return path.join.apply(path, [ROOT].concat(args));
+}
+
+
+exports.hasProcessFlag = hasProcessFlag;
+exports.root = root;
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.dev.js b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.dev.js
new file mode 100644
index 000000000..47af11e21
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.dev.js
@@ -0,0 +1,3 @@
+module.exports = {
+ devtool: 'cheap-module-source-map'
+};
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.js b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.js
new file mode 100644
index 000000000..780f9955f
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.js
@@ -0,0 +1,74 @@
+var path = require('path');
+var webpack = require('webpack');
+var merge = require('extendify')({ isDeep: true, arrays: 'concat' });
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
+var extractCSS = new ExtractTextPlugin('styles.css');
+var ForkCheckerPlugin = require('awesome-typescript-loader').ForkCheckerPlugin;
+var devConfig = require('./webpack.config.dev');
+var prodConfig = require('./webpack.config.prod');
+var isDevelopment = process.env.ASPNETCORE_ENVIRONMENT === 'Production';
+
+console.log("==========Dev Mode = " + isDevelopment + " ============" )
+
+module.exports = merge({
+ resolve: {
+ extensions: ['.js', '.ts']
+ },
+ module: {
+ rules: [
+ { test: /\.ts$/, exclude: [/\.(spec|e2e)\.ts$/], loaders: ['awesome-typescript-loader?forkChecker=true ', 'angular2-template-loader'] },
+ { test: /\.html$/, loader: "html" },
+ { test: /\.css/, loader: extractCSS.extract(['css']) },
+ { test: /\.scss$/, loaders: ['raw-loader', 'sass-loader?sourceMap'] },
+ { test: /\.json$/, loader: 'json-loader' },
+ {
+ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
+ loader: "url?limit=10000&mimetype=application/font-woff"
+ }, {
+ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
+ loader: "url?limit=10000&mimetype=application/font-woff"
+ }, {
+ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+ loader: "url?limit=10000&mimetype=application/octet-stream"
+ }, {
+ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
+ loader: "file"
+ }, {
+ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+ loader: "url?limit=10000&mimetype=image/svg+xml"
+ },
+ {
+ test: /\.(png|jpg|gif)$/,
+ loader: "file"
+ }
+ ]
+ },
+ entry: {
+ 'main': './Client/main.ts'
+ },
+ output: {
+ path: path.join(__dirname, '../wwwroot', 'dist'),
+ filename: '[name].js',
+ publicPath: '/dist/'
+ },
+ profile: true,
+ plugins: [
+ extractCSS,
+ new webpack.DllReferencePlugin({
+ context: __dirname,
+ manifest: require('../wwwroot/dist/vendor-manifest.json')
+ }),
+ // To eliminate warning
+ // https://github.com/AngularClass/angular2-webpack-starter/issues/993
+ new webpack.ContextReplacementPlugin(
+ /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
+ __dirname
+ ),
+ new ForkCheckerPlugin(),
+ new webpack.DefinePlugin({
+ 'process.env': {
+ 'ENV': JSON.stringify(process.env.ASPNETCORE_ENVIRONMENT)
+ }
+ })
+ ]
+}, isDevelopment ? devConfig : prodConfig);
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.prod.js b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.prod.js
new file mode 100644
index 000000000..95277c818
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.prod.js
@@ -0,0 +1,23 @@
+var webpack = require('webpack');
+const WebpackMd5Hash = require('webpack-md5-hash');
+
+module.exports = {
+ devtool: 'source-map',
+ output: {
+ filename: '[name].[chunkhash].bundle.js',
+ sourceMapFilename: '[name].[chunkhash].bundle.map',
+ chunkFilename: '[id].[chunkhash].chunk.js'
+ },
+ plugins: [
+ // new webpack.LoaderOptionsPlugin({
+ // minimize: true,
+ // debug: false
+ // }),
+ new WebpackMd5Hash(),
+ new webpack.optimize.UglifyJsPlugin({
+ beautify: false,
+ comments: false,
+ sourceMap: true
+ })
+ ]
+};
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.vendor.js b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.vendor.js
new file mode 100644
index 000000000..5a3c9f659
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/config/webpack.config.vendor.js
@@ -0,0 +1,73 @@
+var path = require('path');
+var webpack = require('webpack');
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
+var extractCSS = new ExtractTextPlugin('vendor.css');
+var isDevelopment = process.env.ASPNETCORE_ENVIRONMENT === 'Development';
+
+module.exports = {
+ resolve: {
+ extensions: ['.js']
+ },
+ module: {
+ rules: [
+ { test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000' },
+ { test: /\.scss$/i, loader: extractCSS.extract(['css?minimize', 'sass']) },
+ { test: /\.json$/, loader: 'json-loader' }
+ ]
+ },
+ entry: {
+ // polyfills: [
+ // 'core-js/es6/symbol',
+ // 'core-js/es6/object',
+ // 'core-js/es6/function',
+ // 'core-js/es6/parse-int',
+ // 'core-js/es6/parse-float',
+ // 'core-js/es6/number',
+ // 'core-js/es6/math',
+ // 'core-js/es6/string',
+ // 'core-js/es6/date',
+ // 'core-js/es6/array',
+ // 'core-js/es6/regexp',
+ // 'core-js/es6/map',
+ // 'core-js/es6/set',
+ // 'core-js/es6/reflect',
+ // 'core-js/es7/reflect',
+ // 'zone.js/dist/zone'
+ // ],
+ vendor: [
+ 'font-awesome/scss/font-awesome.scss',
+ 'bootstrap/scss/bootstrap.scss',
+ '@angular/common',
+ '@angular/compiler',
+ '@angular/core',
+ '@angular/http',
+ '@angular/forms',
+ '@angular/platform-browser',
+ '@angular/platform-browser-dynamic',
+ '@angular/router'
+ ]
+ },
+ output: {
+ path: path.join(__dirname, '../wwwroot', 'dist'),
+ filename: '[name].js',
+ library: '[name]_[hash]',
+ },
+ plugins: [
+ extractCSS,
+ // To eliminate warning
+ // https://github.com/AngularClass/angular2-webpack-starter/issues/993
+ new webpack.ContextReplacementPlugin(
+ /angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
+ __dirname
+ ),
+ new webpack.DllPlugin({
+ path: path.join(__dirname, '../wwwroot', 'dist', '[name]-manifest.json'),
+ name: '[name]_[hash]'
+ })
+ ].concat(isDevelopment ? [] : [
+ new webpack.optimize.UglifyJsPlugin({
+ beautify: false,
+ comments: false
+ })
+ ])
+};
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/docker-compose.yml b/src/Web/WebSPA/eShopOnContainers.WebSPA/docker-compose.yml
new file mode 100644
index 000000000..34e86d665
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/docker-compose.yml
@@ -0,0 +1,78 @@
+version: '2'
+
+services:
+ webspa:
+ image: eshop/webspa
+ build:
+ context: .
+ dockerfile: Dockerfile
+ environment:
+ - CatalogUrl=http://catalog.api
+ - OrderingUrl=http://ordering.api
+ ports:
+ - "5104:80"
+ depends_on:
+ - catalog.api
+ - identity.data
+
+ catalog.api:
+ image: eshop/catalog.api
+ environment:
+ - ConnectionString=Server=catalog.data;Initial Catalog=CatalogData;User Id=sa;Password=Pass@word
+ expose:
+ - "80"
+ ports:
+ - "5101:80"
+ depends_on:
+ - catalog.data
+
+ catalog.data:
+ image: eshop/mssql-server-private-preview
+ environment:
+ - SA_PASSWORD=Pass@word
+ - ACCEPT_EULA=Y
+ ports:
+ - "5434:1433"
+
+ ordering.api:
+ image: eshop/ordering.api
+ environment:
+ - ConnectionString=Server=ordering.data;Database=Microsoft.eShopOnContainers.Services.OrderingDb;User Id=sa;Password=Pass@word
+ ports:
+ - "5102:80"
+# (Go to Production): For secured/final deployment, remove Ports mapping and
+# leave just the internal expose section
+# expose:
+# - "80"
+ extra_hosts:
+ - "CESARDLBOOKVHD:10.0.75.1"
+ depends_on:
+ - ordering.data
+
+ ordering.data:
+ image: eshop/ordering.data.sqlserver.linux
+ ports:
+ - "5432:1433"
+
+ identity.data:
+ image: eshop/mssql-server-private-preview
+ environment:
+ - SA_PASSWORD=Pass@word
+ - ACCEPT_EULA=Y
+ ports:
+ - "5433:1433"
+
+ basket.api:
+ image: eshop/basket.api
+ environment:
+ - ConnectionString=basket.data
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "5103:80"
+ depends_on:
+ - basket.data
+
+ basket.data:
+ image: redis
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/eShopOnContainers.WebSPA.xproj b/src/Web/WebSPA/eShopOnContainers.WebSPA/eShopOnContainers.WebSPA.xproj
new file mode 100644
index 000000000..e4ab30de3
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/eShopOnContainers.WebSPA.xproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+ true
+
+
+
+ 9842db3a-1391-48c7-a49c-2fabd0a18ac2
+ eShopOnContainers.WebSPA
+
+
+ 2.0
+
+
+
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/hosting.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/hosting.json
new file mode 100644
index 000000000..c42a75b13
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/hosting.json
@@ -0,0 +1,3 @@
+{
+ "server.urls": "http://localhost:5000;http://localhost:5001"
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/package.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/package.json
new file mode 100644
index 000000000..84c93350d
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/package.json
@@ -0,0 +1,109 @@
+{
+ "name": "eshopaspnetnetcoredockerspa",
+ "version": "0.0.0",
+ "private": true,
+ "keywords": [
+ "aspnetcore",
+ "entityframework core",
+ "angular2",
+ "webpack2",
+ "typescript2",
+ "bootstrap4",
+ "docker"
+ ],
+ "author": {
+ "name": "Microsoft",
+ "email": "cesardl@microsoft.com"
+ },
+ "scripts": {
+ "rimraf": "rimraf",
+ "tslint": "tslint",
+ "typedoc": "typedoc",
+ "typings": "typings",
+ "webpack": "webpack",
+ "clean": "npm cache clean && npm run rimraf -- node_modules doc typings coverage wwwroot/dist",
+ "clean:dist": "npm run rimraf -- wwwroot/dist",
+ "preclean:install": "npm run clean",
+ "clean:install": "npm set progress=false && npm install",
+ "preclean:start": "npm run clean",
+ "clean:start": "npm start",
+ "build:vendor": "node node_modules/webpack/bin/webpack.js --config config/webpack.config.vendor.js",
+ "build:main": "node node_modules/webpack/bin/webpack.js --config config/webpack.config.js",
+ "setdev": "set ASPNETCORE_ENVIRONMENT=Development",
+ "setprod": "set ASPNETCORE_ENVIRONMENT=Production",
+ "build:dev": "npm run setdev && npm run clean:dist && npm run build:vendor && npm run build:main",
+ "build:prod": "npm run setprod && npm run clean:dist && npm run build:vendor && npm run build:main",
+ "lint": "npm run tslint \"Client/**/*.ts\"",
+ "docs": "npm run typedoc -- --options typedoc.json --exclude '**/*.spec.ts' ./Client/",
+ "version": "npm run build",
+ },
+ "dependencies": {
+ "@angular/common": "2.1.2",
+ "@angular/compiler": "2.1.2",
+ "@angular/compiler-cli": "2.1.2",
+ "@angular/core": "2.1.2",
+ "@angular/forms": "2.1.2",
+ "@angular/http": "2.1.2",
+ "@angular/platform-browser": "2.1.2",
+ "@angular/platform-browser-dynamic": "2.1.2",
+ "@angular/platform-server": "2.1.2",
+ "@angular/router": "3.1.2",
+ "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.11",
+ "aspnet-prerendering": "1.0.7",
+ "aspnet-webpack": "1.0.24",
+ "bootstrap": "4.0.0-alpha.5",
+ "core-js": "2.4.1",
+ "font-awesome": "4.6.3",
+ "isomorphic-fetch": "2.2.1",
+ "ng2-translate": "4.0.0",
+ "normalize.css": "5.0.0",
+ "preboot": "4.5.2",
+ "rxjs": "5.0.0-beta.12",
+ "zone.js": "0.6.26"
+ },
+ "devDependencies": {
+ "@types/core-js": "0.9.34",
+ "@types/hammerjs": "2.0.33",
+ "@types/jasmine": "2.5.35",
+ "@types/node": "6.0.45",
+ "@types/protractor": "1.5.20",
+ "@types/selenium-webdriver": "2.44.26",
+ "@types/sinon": "1.16.31",
+ "@types/source-map": "0.1.28",
+ "@types/uglify-js": "2.6.28",
+ "@types/webpack": "1.12.35",
+ "angular2-template-loader": "0.6.0",
+ "awesome-typescript-loader": "2.2.4",
+ "codelyzer": "1.0.0-beta.3",
+ "copy-webpack-plugin": "^4.0.0",
+ "css": "2.2.1",
+ "css-loader": "0.25.0",
+ "es6-promise": "3.2.1",
+ "es6-promise-loader": "1.0.2",
+ "expose-loader": "0.7.1",
+ "extendify": "1.0.0",
+ "extract-text-webpack-plugin": "2.0.0-beta.4",
+ "file-loader": "0.9.0",
+ "html-loader": "0.4.4",
+ "html-webpack-plugin": "^2.24.1",
+ "json-loader": "0.5.4",
+ "node-sass": "3.9.3",
+ "parse5": "2.1.5",
+ "raw-loader": "0.5.1",
+ "rimraf": "2.5.4",
+ "sass-loader": "4.0.2",
+ "source-map-loader": "0.1.5",
+ "style-loader": "0.13.1",
+ "ts-helpers": "1.1.1",
+ "ts-node": "1.4.3",
+ "tslint": "3.15.1",
+ "tslint-loader": "2.1.5",
+ "typedoc": "0.5.0",
+ "typescript": "2.0.6",
+ "url-loader": "0.5.7",
+ "webpack": "2.1.0-beta.25",
+ "webpack-externals-plugin": "1.0.0",
+ "webpack-hot-middleware": "2.13.0",
+ "webpack-md5-hash": "0.0.5"
+ }
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/project.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/project.json
new file mode 100644
index 000000000..5482f5222
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/project.json
@@ -0,0 +1,116 @@
+{
+ "userSecretsId": "aspnetcorespa-c23d27a4-eb88-4b18-9b77-2a93f3b15119",
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0",
+ "type": "platform"
+ },
+ "Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
+ "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0",
+ "Microsoft.AspNetCore.Diagnostics": "1.0.0",
+ "Microsoft.AspNetCore.Mvc": "1.0.1",
+ "Microsoft.AspNetCore.Cors": "1.0.0",
+ "Microsoft.AspNetCore.Antiforgery": "1.0.1",
+ "Microsoft.AspNetCore.Authorization": "1.0.0",
+ "Newtonsoft.Json": "9.0.1",
+ "Webpack": "3.0.0",
+ "Microsoft.AspNetCore.AngularServices": "1.0.0-beta-000014",
+ "Microsoft.AspNetCore.Razor.Tools": {
+ "version": "1.0.0-preview2-final",
+ "type": "build"
+ },
+ "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
+ "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
+ "Microsoft.AspNetCore.StaticFiles": "1.0.0",
+ "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
+ "Microsoft.Extensions.Configuration.Json": "1.0.0",
+ "Microsoft.Extensions.Logging": "1.0.0",
+ "Microsoft.Extensions.Logging.Console": "1.0.0",
+ "Microsoft.Extensions.Logging.Debug": "1.0.0",
+ "Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
+ "version": "1.0.0-preview2-final",
+ "type": "build"
+ },
+ "Microsoft.VisualStudio.Web.CodeGenerators.Mvc": {
+ "version": "1.0.0-preview2-final",
+ "type": "build"
+ },
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.0"
+ },
+ "tools": {
+ "Microsoft.DotNet.Watcher.Tools": {
+ "version": "1.0.0-*",
+ "imports": "portable-net451+win8"
+ },
+ "Microsoft.AspNetCore.Razor.Tools": {
+ "version": "1.0.0-preview2-final",
+ "imports": "portable-net45+win8+dnxcore50"
+ },
+ "Microsoft.AspNetCore.Server.IISIntegration.Tools": {
+ "version": "1.0.0-preview2-final",
+ "imports": "portable-net45+win8+dnxcore50"
+ },
+ "Microsoft.Extensions.SecretManager.Tools": {
+ "version": "1.0.0-preview2-final",
+ "imports": "portable-net45+win8+dnxcore50"
+ },
+ "Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
+ "version": "1.0.0-preview2-final",
+ "imports": [
+ "portable-net45+win8+dnxcore50",
+ "portable-net45+win8"
+ ]
+ }
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "imports": [
+ "dotnet5.6",
+ "portable-net45+win8"
+ ]
+ }
+ },
+ "buildOptions": {
+ "emitEntryPoint": true,
+ "preserveCompilationContext": true,
+ "compile": {
+ "exclude": [
+ "node_modules",
+ "Client"
+ ]
+ },
+ "debugType": "portable"
+ },
+ "runtimeOptions": {
+ "configProperties": {
+ "System.GC.Server": true
+ }
+ },
+ "publishOptions": {
+ "include": [
+ "appsettings.json",
+ "Client",
+ "typings",
+ "Views",
+ "tsconfig.json",
+ "tsd.json",
+ "web.config",
+ "config",
+ "wwwroot",
+ "dockerfile"
+ ]
+ },
+ "scripts": {
+ // "prepublish": [
+ // "npm install",
+ // "node node_modules/webpack/bin/webpack.js --config config/webpack.config.vendor.js",
+ // "node node_modules/webpack/bin/webpack.js --config config/webpack.config.js"
+ // ],
+ "postpublish": [
+ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%"
+ ]
+ },
+ "tooling": {
+ "defaultNamespace": "eShopOnContainers.SPA"
+ }
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/tsconfig.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/tsconfig.json
new file mode 100644
index 000000000..e04adfb21
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "declaration": false,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "sourceMap": true,
+ "strictNullChecks": false,
+ "baseUrl": "./src",
+ "paths": {},
+ "lib": [
+ "dom",
+ "es6"
+ ],
+ "types": [
+ "hammerjs",
+ "jasmine",
+ "node",
+ "protractor",
+ "selenium-webdriver",
+ "source-map",
+ "uglify-js",
+ "webpack"
+ ]
+ },
+ "exclude": [
+ "node_modules",
+ "wwwroot"
+ ],
+ "awesomeTypescriptLoaderOptions": {
+ "forkChecker": true,
+ "useWebpackText": true
+ },
+ "compileOnSave": false,
+ "buildOnSave": false,
+ "atom": {
+ "rewriteTsconfig": false
+ }
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/tslint.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/tslint.json
new file mode 100644
index 000000000..10185a3ad
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/tslint.json
@@ -0,0 +1,178 @@
+{
+ "rulesDirectory": [
+ "node_modules/codelyzer"
+ ],
+ "rules": {
+ "directive-selector-name": [
+ true,
+ "camelCase"
+ ],
+ "component-selector-name": [
+ true,
+ "kebab-case"
+ ],
+ "directive-selector-type": [
+ true,
+ "attribute"
+ ],
+ "component-selector-type": [
+ true,
+ "element"
+ ],
+ "directive-selector-prefix": [
+ true,
+ "appd"
+ ],
+ "component-selector-prefix": [
+ true,
+ "appc"
+ ],
+ "use-input-property-decorator": true,
+ "use-output-property-decorator": true,
+ "use-host-property-decorator": true,
+ "no-attribute-parameter-decorator": true,
+ "no-input-rename": true,
+ "no-output-rename": true,
+ "no-forward-ref": true,
+ "use-life-cycle-interface": true,
+ "use-pipe-transform-interface": true,
+ "pipe-naming": [
+ true,
+ "camelCase",
+ "appf"
+ ],
+ "component-class-suffix": true,
+ "directive-class-suffix": true,
+ "import-destructuring-spacing": true,
+ "member-access": false,
+ "member-ordering": [
+ true,
+ "public-before-private",
+ "static-before-instance",
+ "variables-before-functions"
+ ],
+ "no-any": false,
+ "no-inferrable-types": false,
+ "no-internal-module": true,
+ "no-var-requires": false,
+ "typedef": false,
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ },
+ {
+ "call-signature": "space",
+ "index-signature": "space",
+ "parameter": "space",
+ "property-declaration": "space",
+ "variable-declaration": "space"
+ }
+ ],
+ "ban": false,
+ "curly": false,
+ "forin": true,
+ "label-position": true,
+ "label-undefined": true,
+ "no-arg": true,
+ "no-bitwise": true,
+ "no-conditional-assignment": true,
+ "no-console": [
+ true,
+ "debug",
+ "info",
+ "time",
+ "timeEnd",
+ "trace"
+ ],
+ "no-construct": true,
+ "no-debugger": true,
+ "no-duplicate-key": true,
+ "no-duplicate-variable": true,
+ "no-empty": false,
+ "no-eval": true,
+ "no-null-keyword": true,
+ "no-shadowed-variable": true,
+ "no-string-literal": true,
+ "no-switch-case-fall-through": true,
+ "no-unreachable": true,
+ "no-unused-expression": true,
+ "no-unused-variable": false,
+ "no-use-before-declare": true,
+ "no-var-keyword": true,
+ "radix": true,
+ "switch-default": true,
+ "triple-equals": [
+ true,
+ "allow-null-check"
+ ],
+ "use-strict": [
+ true,
+ "check-module"
+ ],
+ "eofline": true,
+ "indent": [
+ true,
+ "spaces"
+ ],
+ "max-line-length": [
+ true,
+ 200
+ ],
+ "no-require-imports": false,
+ "no-trailing-whitespace": true,
+ "object-literal-sort-keys": false,
+ "trailing-comma": [
+ true,
+ {
+ "multiline": "never",
+ "singleline": "never"
+ }
+ ],
+ "align": false,
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "interface-name": false,
+ "jsdoc-format": true,
+ "no-consecutive-blank-lines": false,
+ "no-constructor-vars": false,
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-catch",
+ "check-else",
+ "check-finally",
+ "check-whitespace"
+ ],
+ "quotemark": [
+ true,
+ "single",
+ "avoid-escape"
+ ],
+ "semicolon": [
+ true,
+ "always"
+ ],
+ "variable-name": [
+ true,
+ "check-format",
+ "allow-leading-underscore",
+ "ban-keywords"
+ ],
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-separator",
+ "check-type"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/typedoc.json b/src/Web/WebSPA/eShopOnContainers.WebSPA/typedoc.json
new file mode 100644
index 000000000..5546b0672
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/typedoc.json
@@ -0,0 +1,15 @@
+{
+ "mode": "modules",
+ "out": "doc",
+ "theme": "default",
+ "ignoreCompilerErrors": "true",
+ "experimentalDecorators": "true",
+ "emitDecoratorMetadata": "true",
+ "target": "ES5",
+ "moduleResolution": "node",
+ "preserveConstEnums": "true",
+ "stripInternal": "true",
+ "suppressExcessPropertyErrors": "true",
+ "suppressImplicitAnyIndexErrors": "true",
+ "module": "commonjs"
+}
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/web.config b/src/Web/WebSPA/eShopOnContainers.WebSPA/web.config
new file mode 100644
index 000000000..b70ce7e43
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/web.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/favicon.ico b/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/favicon.ico
new file mode 100644
index 000000000..3bb790961
Binary files /dev/null and b/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/favicon.ico differ
diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/web.config b/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/web.config
new file mode 100644
index 000000000..e70a7778d
--- /dev/null
+++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/wwwroot/web.config
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+