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: `` +}) +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 @@ +
+
    +
  • + {{error}} +
  • +
+
\ 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 @@ +
+
+
+

+ © 2015-2016 {{'title' | translate}} Company +

+
+
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 @@ + + + + + + + + +