diff --git a/.gitignore b/.gitignore index 93b0888d74..f27c267e5a 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,8 @@ nuget.exe build/ *net45.csproj *k10.csproj -App_Data/ \ No newline at end of file +App_Data/ +bower_components +node_modules +*.sln.ide +*/*.Spa/public/* \ No newline at end of file diff --git a/MusicStore.sln b/MusicStore.sln index 20a876ffbc..cb0f0b62f7 100644 --- a/MusicStore.sln +++ b/MusicStore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21629.0 +VisualStudioVersion = 14.0.21706.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcMusicStore", "src\MvcMusicStore\MvcMusicStore.csproj", "{25CE8290-EF24-4818-B009-68DC903163D3}" EndProject @@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{446215 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore", "src\MusicStore\MusicStore.kproj", "{A06F8BE0-C66D-4650-A4E9-A639212BC507}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcMusicStore.Spa", "src\MvcMusicStore.Spa\MvcMusicStore.Spa.csproj", "{408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,16 +31,26 @@ Global {25CE8290-EF24-4818-B009-68DC903163D3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {25CE8290-EF24-4818-B009-68DC903163D3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {25CE8290-EF24-4818-B009-68DC903163D3}.Release|x86.ActiveCfg = Release|Any CPU - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Any CPU.ActiveCfg = Debug|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Mixed Platforms.Build.0 = Debug|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|x86.ActiveCfg = Debug|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|x86.Build.0 = Debug|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Any CPU.ActiveCfg = Release|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Mixed Platforms.ActiveCfg = Release|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Mixed Platforms.Build.0 = Release|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|x86.ActiveCfg = Release|x86 - {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|x86.Build.0 = Release|x86 + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Debug|x86.ActiveCfg = Debug|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Any CPU.Build.0 = Release|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {A06F8BE0-C66D-4650-A4E9-A639212BC507}.Release|x86.ActiveCfg = Release|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Debug|x86.ActiveCfg = Debug|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Any CPU.Build.0 = Release|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MusicStore/MusicStore.kproj b/src/MusicStore/MusicStore.kproj index 77fc600ef8..1a0ef98df2 100644 --- a/src/MusicStore/MusicStore.kproj +++ b/src/MusicStore/MusicStore.kproj @@ -73,6 +73,8 @@ + + diff --git a/src/MusicStore/web.Debug.config b/src/MusicStore/web.Debug.config new file mode 100644 index 0000000000..2e302f9f95 --- /dev/null +++ b/src/MusicStore/web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/MusicStore/web.Release.config b/src/MusicStore/web.Release.config new file mode 100644 index 0000000000..c35844462b --- /dev/null +++ b/src/MusicStore/web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Apis/AlbumsApiController.cs b/src/MvcMusicStore.Spa/Apis/AlbumsApiController.cs new file mode 100644 index 0000000000..af72158df7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Apis/AlbumsApiController.cs @@ -0,0 +1,157 @@ +using System.Data.Entity; +using System.Linq; +using System.Web.Helpers; +using System.Web.Mvc; +using MvcMusicStore.Infrastructure; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Apis +{ + public class AlbumsApiController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + [Route("api/albums")] + public ActionResult Paged(int page = 1, int pageSize = 50, string sortBy = null) + { + var pagedAlbums = _storeContext.Albums + .Include(a => a.Genre) + .Include(a => a.Artist) + .SortBy(sortBy, a => a.Title) + .ToPagedList(page, pageSize); + + return new SmartJsonResult + { + Data = pagedAlbums + }; + } + + [Route("api/albums/all")] + public ActionResult All() + { + return new SmartJsonResult + { + Data = _storeContext.Albums + .Include(a => a.Genre) + .Include(a => a.Artist) + .OrderBy(a => a.Title) + }; + } + + [Route("api/albums/mostPopular")] + public ActionResult MostPopular(int count = 6) + { + count = count > 0 && count < 20 ? count : 6; + + return new SmartJsonResult + { + Data = _storeContext.Albums + .OrderByDescending(a => a.OrderDetails.Count()) + .Take(count) + }; + } + + [Route("api/albums/{albumId:int}")] + public ActionResult Details(int albumId) + { + return new SmartJsonResult + { + Data = _storeContext.Albums + .Include(a => a.Artist) + .Include(a => a.Genre) + .SingleOrDefault(a => a.AlbumId == albumId) + }; + } + + [Route("api/albums")] + [HttpPost] + [Authorize(Roles = "Administrator")] + public ActionResult CreateAlbum() + { + var album = new Album(); + + if (!TryUpdateModel(album, prefix: null, includeProperties: null, excludeProperties: new[] { "Genre", "Artist", "OrderDetails" })) + { + // Return the model errors + return new ApiResult(ModelState); + } + + // Save the changes to the DB + _storeContext.Albums.Add(album); + _storeContext.SaveChanges(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Data = album.AlbumId, + Message = "Album created successfully." + }; + } + + [Route("api/albums/{albumId:int}/update")] + [HttpPut] + [Authorize(Roles = "Administrator")] + public ActionResult UpdateAlbum(int albumId) + { + var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId); + + if (album == null) + { + return new ApiResult + { + StatusCode = 404, + Message = string.Format("The album with ID {0} was not found.", albumId) + }; + } + + if (!TryUpdateModel(album, prefix: null, includeProperties: null, excludeProperties: new[] { "Genre", "Artist", "OrderDetails" })) + { + // Return the model errors + return new ApiResult(ModelState); + } + + // Save the changes to the DB + _storeContext.SaveChanges(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Message = "Album updated successfully." + }; + } + + [Route("api/albums/{albumId:int}")] + [HttpDelete] + [Authorize(Roles = "Administrator")] + public ActionResult DeleteAlbum(int albumId) + { + var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId); + + if (album != null) + { + _storeContext.Albums.Remove(album); + + // Save the changes to the DB + _storeContext.SaveChanges(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + } + + return new ApiResult + { + Message = "Album deleted successfully." + }; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/src/MvcMusicStore.Spa/Apis/ArtistsApiController.cs b/src/MvcMusicStore.Spa/Apis/ArtistsApiController.cs new file mode 100644 index 0000000000..62b3c76e6f --- /dev/null +++ b/src/MvcMusicStore.Spa/Apis/ArtistsApiController.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Apis +{ + public class ArtistsApiController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + [Route("api/artists/lookup")] + public ActionResult Lookup() + { + return new SmartJsonResult + { + Data = _storeContext.Artists.OrderBy(a => a.Name).ToList() + }; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Apis/GenresApiController.cs b/src/MvcMusicStore.Spa/Apis/GenresApiController.cs new file mode 100644 index 0000000000..1b6addb973 --- /dev/null +++ b/src/MvcMusicStore.Spa/Apis/GenresApiController.cs @@ -0,0 +1,67 @@ +using System.Data.Entity; +using System.Linq; +using System.Web.Mvc; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Apis +{ + public class GenresApiController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + [Route("api/genres/lookup")] + public ActionResult Lookup() + { + return new SmartJsonResult + { + Data = _storeContext.Genres.Select(g => new { g.GenreId, g.Name }) + }; + } + + [Route("api/genres/menu")] + public ActionResult GenreMenuList(int count = 9) + { + count = count > 0 && count < 20 ? count : 9; + + return new SmartJsonResult + { + Data = _storeContext.Genres + .OrderByDescending(g => g.Albums.Sum(a => a.OrderDetails.Sum(od => od.Quantity))) + .Take(count) + }; + } + + [Route("api/genres")] + public ActionResult GenreList() + { + return new SmartJsonResult + { + Data = _storeContext.Genres + .Include(g => g.Albums) + .OrderBy(g => g.Name) + }; + } + + [Route("api/genres/{genreId:int}/albums")] + public ActionResult GenreAlbums(int genreId) + { + return new SmartJsonResult + { + Data = _storeContext.Albums + .Where(a => a.GenreId == genreId) + .Include(a => a.Genre) + .Include(a => a.Artist) + .OrderBy(a => a.Genre.Name) + }; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/App_Start/FilterConfig.cs b/src/MvcMusicStore.Spa/App_Start/FilterConfig.cs new file mode 100644 index 0000000000..a9fc460edc --- /dev/null +++ b/src/MvcMusicStore.Spa/App_Start/FilterConfig.cs @@ -0,0 +1,13 @@ +using System.Web; +using System.Web.Mvc; + +namespace MvcMusicStore +{ + public class FilterConfig + { + public static void RegisterGlobalFilters(GlobalFilterCollection filters) + { + filters.Add(new HandleErrorAttribute()); + } + } +} diff --git a/src/MvcMusicStore.Spa/App_Start/RouteConfig.cs b/src/MvcMusicStore.Spa/App_Start/RouteConfig.cs new file mode 100644 index 0000000000..fda7541ccc --- /dev/null +++ b/src/MvcMusicStore.Spa/App_Start/RouteConfig.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; + +namespace MvcMusicStore +{ + public class RouteConfig + { + public static void RegisterRoutes(RouteCollection routes) + { + routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); + + routes.RouteExistingFiles = true; + + routes.MapMvcAttributeRoutes(); + + routes.MapRoute( + name: "Default", + url: "{controller}/{action}/{id}", + defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } + ); + } + } +} diff --git a/src/MvcMusicStore.Spa/App_Start/Startup.App.cs b/src/MvcMusicStore.Spa/App_Start/Startup.App.cs new file mode 100644 index 0000000000..053b2a9a30 --- /dev/null +++ b/src/MvcMusicStore.Spa/App_Start/Startup.App.cs @@ -0,0 +1,55 @@ +using System.Configuration; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; +using MvcMusicStore.Models; +using Owin; + +namespace MvcMusicStore +{ + public partial class Startup + { + private const string RoleName = "Administrator"; + + public void ConfigureApp(IAppBuilder app) + { + using (var context = new MusicStoreEntities()) + { + context.Database.Delete(); + context.Database.Create(); + + new SampleData().Seed(context); + } + + CreateAdminUser().Wait(); + } + + private async Task CreateAdminUser() + { + var username = ConfigurationManager.AppSettings["DefaultAdminUsername"]; + var password = ConfigurationManager.AppSettings["DefaultAdminPassword"]; + + using (var context = new ApplicationDbContext()) + { + var userManager = new UserManager(new UserStore(context)); + var roleManager = new RoleManager(new RoleStore(context)); + + var role = new IdentityRole(RoleName); + + var result = await roleManager.RoleExistsAsync(RoleName); + if (!result) + { + await roleManager.CreateAsync(role); + } + + var user = await userManager.FindByNameAsync(username); + if (user == null) + { + user = new ApplicationUser { UserName = username }; + await userManager.CreateAsync(user, password); + await userManager.AddToRoleAsync(user.Id, RoleName); + } + } + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/App_Start/Startup.Auth.cs b/src/MvcMusicStore.Spa/App_Start/Startup.Auth.cs new file mode 100644 index 0000000000..6a7c520351 --- /dev/null +++ b/src/MvcMusicStore.Spa/App_Start/Startup.Auth.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNet.Identity; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Owin; + +namespace MvcMusicStore +{ + public partial class Startup + { + // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 + public void ConfigureAuth(IAppBuilder app) + { + // Enable the application to use a cookie to store information for the signed in user + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, + LoginPath = new PathString("/Account/Login") + }); + // Use a cookie to temporarily store information about a user logging in with a third party login provider + app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + + // Uncomment the following lines to enable logging in with third party login providers + //app.UseMicrosoftAccountAuthentication( + // clientId: "", + // clientSecret: ""); + + //app.UseTwitterAuthentication( + // consumerKey: "", + // consumerSecret: ""); + + //app.UseFacebookAuthentication( + // appId: "", + // appSecret: ""); + + //app.UseGoogleAuthentication(); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/Site.less b/src/MvcMusicStore.Spa/Client/Site.less new file mode 100644 index 0000000000..64438d2afd --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/Site.less @@ -0,0 +1,82 @@ +@import '../bower_components/bootstrap/less/bootstrap.less'; + +[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + display: none !important; +} + +.nav, .pagination, .carousel, .panel-title a { + cursor: pointer; +} + +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Set padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Set width on the form input elements since they're 100% wide by default */ +input, +select, +textarea { + /*max-width: 280px;*/ +} + +/* styles for validation helpers */ +.field-validation-error { + color: #b94a48; +} + +.field-validation-valid { + display: none; +} + +input.input-validation-error { + border: 1px solid #b94a48; +} + +input[type="checkbox"].input-validation-error { + border: 0 none; +} + +.validation-summary-errors { + color: #b94a48; +} + +.validation-summary-valid { + display: none; +} + + +/* Music Store additions */ + +ul#album-list li { + height: 160px; +} + +ul#album-list li img:hover { + box-shadow: 1px 1px 7px #777; +} + +ul#album-list li img { + max-width: 100px; + max-height: 100px; + box-shadow: 1px 1px 5px #999; + border: none; + padding: 0; +} + +ul#album-list li a, ul#album-details li a { + text-decoration:none; +} + +ul#album-list li a:hover { + background: none; + -webkit-text-shadow: 1px 1px 2px #bbb; + text-shadow: 1px 1px 2px #bbb; + color: #363430; +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/favicon.ico b/src/MvcMusicStore.Spa/Client/favicon.ico new file mode 100644 index 0000000000..a3a799985c Binary files /dev/null and b/src/MvcMusicStore.Spa/Client/favicon.ico differ diff --git a/src/MvcMusicStore.Spa/Client/images/home-showcase.png b/src/MvcMusicStore.Spa/Client/images/home-showcase.png new file mode 100644 index 0000000000..258c19d3cd Binary files /dev/null and b/src/MvcMusicStore.Spa/Client/images/home-showcase.png differ diff --git a/src/MvcMusicStore.Spa/Client/images/logo.png b/src/MvcMusicStore.Spa/Client/images/logo.png new file mode 100644 index 0000000000..d334c86256 Binary files /dev/null and b/src/MvcMusicStore.Spa/Client/images/logo.png differ diff --git a/src/MvcMusicStore.Spa/Client/images/logo.svg b/src/MvcMusicStore.Spa/Client/images/logo.svg new file mode 100644 index 0000000000..ec3cd6aa5b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/images/logo.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Client/images/placeholder.png b/src/MvcMusicStore.Spa/Client/images/placeholder.png new file mode 100644 index 0000000000..1f73dbb43d Binary files /dev/null and b/src/MvcMusicStore.Spa/Client/images/placeholder.png differ diff --git a/src/MvcMusicStore.Spa/Client/images/placeholder.svg b/src/MvcMusicStore.Spa/Client/images/placeholder.svg new file mode 100644 index 0000000000..07d58202df --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/images/placeholder.svg @@ -0,0 +1,112 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin.Catalog.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin.Catalog.ng.ts new file mode 100644 index 0000000000..2af77e119e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin.Catalog.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.Admin.Catalog { + angular.module("MusicStore.Admin.Catalog", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml new file mode 100644 index 0000000000..c33efb494e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ng.ts new file mode 100644 index 0000000000..97f175f70f --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ng.ts @@ -0,0 +1,30 @@ +module MusicStore.Admin.Catalog { + export interface IAlbumDeleteModalViewModel { + album: Models.IAlbum; + ok(); + cancel(); + } + + // We don't register this controller with Angular's DI system because the $modal service + // will create and resolve its dependencies directly + + //@NgController(skip=true) + export class AlbumDeleteModalController implements IAlbumDeleteModalViewModel { + private _modalInstance: ng.ui.bootstrap.IModalServiceInstance; + + constructor($modalInstance: ng.ui.bootstrap.IModalServiceInstance, album: Models.IAlbum) { + this._modalInstance = $modalInstance; + this.album = album; + } + + public album: Models.IAlbum; + + public ok() { + this._modalInstance.close(true); + } + + public cancel() { + this._modalInstance.dismiss("cancel"); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts new file mode 100644 index 0000000000..9e711d12af --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts @@ -0,0 +1,30 @@ +module MusicStore.Admin.Catalog { + export interface IAlbumDeleteModalViewModel { + album: Models.IAlbum; + ok(); + cancel(); + } + + // We don't register this controller with Angular's DI system because the $modal service + // will create and resolve its dependencies directly + + //@NgController(skip=true) + export class AlbumDeleteModalController implements IAlbumDeleteModalViewModel { + private _modalInstance: ng.ui.bootstrap.IModalServiceInstance; + + constructor($modalInstance: ng.ui.bootstrap.IModalServiceInstance, album: Models.IAlbum) { + this._modalInstance = $modalInstance; + this.album = album; + } + + public album: Models.IAlbum; + + public ok() { + this._modalInstance.close(true); + } + + public cancel() { + this._modalInstance.dismiss("cancel"); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml new file mode 100644 index 0000000000..43060109d1 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml @@ -0,0 +1,58 @@ +@model MvcMusicStore.Models.Album + +
+

Album Details

+
+ +
+
+ @Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Artist.Name }}

+
+
+ +
+ @Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Genre.Name }}

+
+
+ +
+ @Html.LabelFor(m => m.Title, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Title }}

+
+
+ +
+ @Html.LabelFor(m => m.Price, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Price | currency }}

+
+
+ +
+ @Html.LabelFor(m => m.AlbumArtUrl, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.AlbumArtUrl }}

+
+
+ +
+ +
+

Album Art

+
+
+ +
+
+ Edit + + Back to List +
+
+
+
diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ng.ts new file mode 100644 index 0000000000..12c745d855 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ng.ts @@ -0,0 +1,70 @@ +/// + +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + deleteAlbum(); + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _albumApi: AlbumApi.IAlbumApiService; + private _viewAlert: ViewAlert.IViewAlertService; + + constructor($routeParams: IAlbumDetailsRouteParams, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService) { + + this._modal = $modal; + this._location = $location; + this._albumApi = albumApi; + this._viewAlert = viewAlert; + + albumApi.getAlbumDetails($routeParams.albumId).then(album => this.album = album); + } + + public album: Models.IAlbum; + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + } + + angular.module("MusicStore.Admin.Catalog") + .controller("MusicStore.Admin.Catalog.AlbumDetailsController", [ + "$routeParams", + "$modal", + "$location", + "MusicStore.AlbumApi.IAlbumApiService", + "MusicStore.ViewAlert.IViewAlertService", + AlbumDetailsController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts new file mode 100644 index 0000000000..d70c340470 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts @@ -0,0 +1,58 @@ +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + deleteAlbum(); + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _albumApi: AlbumApi.IAlbumApiService; + private _viewAlert: ViewAlert.IViewAlertService; + + constructor($routeParams: IAlbumDetailsRouteParams, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService) { + + this._modal = $modal; + this._location = $location; + this._albumApi = albumApi; + this._viewAlert = viewAlert; + + albumApi.getAlbumDetails($routeParams.albumId).then(album => this.album = album); + } + + public album: Models.IAlbum; + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml new file mode 100644 index 0000000000..f997c2def4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml @@ -0,0 +1,99 @@ +@model MvcMusicStore.Models.Album + +
+

Album {{ viewModel.mode | titlecase }}

+
+ + + {{ viewModel.alert.message }} +
    +
  • {{ modelError.ErrorMessage }}
  • +
+
+ +
+
+ @Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" }) +
+
+
+ @Html.ngDropDownListFor(m => m.ArtistId, m => m.Artist.Name, source: "viewModel.artists", nullOption: "-- choose Artist --", + htmlAttributes: new { @class = "form-control", ng_model = "viewModel.album.ArtistId", ng_disabled = "viewModel.disabled || viewModel.artists.length < 2" }) +
+
+ @Html.ngValidationMessageFor(m => m.ArtistId, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" }) +
+
+
+ @Html.ngDropDownListFor(m => m.GenreId, m => m.Genre.Name, source: "viewModel.genres", nullOption: "-- choose Genre --", + htmlAttributes: new { @class = "form-control", ng_model = "viewModel.album.GenreId", ng_disabled = "viewModel.disabled || viewModel.genres.length < 2" }) +
+
+ @Html.ngValidationMessageFor(m => m.GenreId, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Title, new { @class = "control-label col-md-2" }) +
+
+
+ @Html.ngTextBoxFor(m => m.Title, new { @class = "form-control", ng_model = "viewModel.album.Title", ng_disabled = "viewModel.disabled" }) +
+
+ @Html.ngValidationMessageFor(model => model.Title, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" }) +
+
+
+
+ $ + @Html.ngTextBoxFor(m => m.Price, new { @class = "form-control", ng_model = "viewModel.album.Price", ng_disabled = "viewModel.disabled" }) +
+
+
+ @Html.ngValidationMessageFor(model => model.Price, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.AlbumArtUrl, new { @class = "control-label col-md-2" }) +
+
+
+ @Html.ngTextBoxFor(m => m.AlbumArtUrl, new { @class = "form-control", ng_model = "viewModel.album.AlbumArtUrl", ng_disabled = "viewModel.disabled" }) +
+
+ @Html.ngValidationMessageFor(model => model.AlbumArtUrl, "editAlbum", new { @class = "field-validation-error" }) +
+
+ +
+
+ Album Art +
+
+ +
+
+ + + Back to List +
+
+
+
\ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ng.ts new file mode 100644 index 0000000000..c3b0248105 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ng.ts @@ -0,0 +1,205 @@ +/// + +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + mode: string; + albumId: number; + } + + interface IAlbumDetailsViewModel { + mode: string; // edit or new + disabled: boolean; + album: Models.IAlbum; + alert: Models.IAlert; + artists: Array; + genres: Array; + save(); + clearAlert(); + } + + class AlbumEditController implements IAlbumDetailsViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _artistApi: ArtistApi.IArtistApiService; + private _genreApi: GenreApi.IGenreApiService; + private _viewAlert: ViewAlert.IViewAlertService; + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor($routeParams: IAlbumDetailsRouteParams, + albumApi: AlbumApi.IAlbumApiService, + artistApi: ArtistApi.IArtistApiService, + genreApi: GenreApi.IGenreApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + $timeout: ng.ITimeoutService, + $q: ng.IQService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._artistApi = artistApi; + this._genreApi = genreApi; + this._viewAlert = viewAlert; + this._modal = $modal; + this._location = $location; + this._timeout = $timeout; + this._log = $log; + + this.mode = $routeParams.mode; + + this.alert = viewAlert.alert; + + artistApi.getArtistsLookup().then(artists => this.artists = artists); + genreApi.getGenresLookup().then(genres => this.genres = genres); + + if (this.mode.toLowerCase() === "edit") { + // TODO: Handle album load failure + albumApi.getAlbumDetails($routeParams.albumId).then(album => { + this.album = album; + + // Pre-load the lookup arrays with the current values if not set yet + this.genres = this.genres || [album.Genre]; + this.artists = this.artists || [album.Artist]; + + this.disabled = false; + }); + } else { + this.disabled = false; + } + } + + public mode: string; + + public disabled = true; + + public album: Models.IAlbum; + + public alert: Models.IAlert; + + public artists: Array; + + public genres: Array; + + public save() { + this.disabled = true; + + var apiMethod = this.mode.toLowerCase() === "edit" ? this._albumApi.updateAlbum : this._albumApi.createAlbum; + apiMethod = apiMethod.bind(this._albumApi); + + apiMethod(this.album).then( + // Success + response => { + var alert = { + type: Models.AlertType.success, + message: response.data.Message + }; + + // TODO: Do we need to destroy this timeout on controller unload? + this._timeout(() => this.alert !== alert || this.clearAlert(), 3000); + + if (this.mode.toLowerCase() === "new") { + this._log.info("Created album successfully!"); + + var albumId: number = response.data.Data; + + this._viewAlert.alert = alert; + + // Reload the view with the new album ID + this._location.path("/albums/" + albumId + "/edit").replace(); + } else { + this.alert = alert; + this.disabled = false; + this._log.info("Updated album " + this.album.AlbumId + " successfully!"); + } + }, + // Error + response => { + // TODO: Make this common logic, e.g. base controller class, injected helper service, etc. + if (response.status === 400) { + // We made a bad request + if (response.data && response.data.ModelErrors) { + // The server says the update failed validation + // TODO: Map errors back to client validators and/or summary + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message, + modelErrors: response.data.ModelErrors + }; + this.disabled = false; + } else { + // Some other bad request, just show the message + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } + } else if (response.status === 404) { + // The album wasn't found, probably deleted. Leave the form disabled and show error message. + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } else if (response.status === 401) { + // We need to authenticate again + // TODO: Should we just redirect to login page, show a message with a link, or something else + this.alert = { + type: Models.AlertType.danger, + message: "Your session has timed out. Please log in and try again." + }; + } else if (!response.status) { + // Request timed out or no response from server or worse + this._log.error("Error updating album " + this.album.AlbumId); + this._log.error(response); + this.alert = { type: Models.AlertType.danger, message: "An unexpected error occurred. Please try again." }; + this.disabled = false; + } + }); + } + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + } + + angular.module("MusicStore.Admin.Catalog") + .controller("MusicStore.Admin.Catalog.AlbumEditController", [ + "$routeParams", + "MusicStore.AlbumApi.IAlbumApiService", + "MusicStore.ArtistApi.IArtistApiService", + "MusicStore.GenreApi.IGenreApiService", + "MusicStore.ViewAlert.IViewAlertService", + "$modal", + "$location", + "$timeout", + "$q", + "$log", + AlbumEditController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts new file mode 100644 index 0000000000..f46fcab8ca --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts @@ -0,0 +1,188 @@ +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + mode: string; + albumId: number; + } + + interface IAlbumDetailsViewModel { + mode: string; // edit or new + disabled: boolean; + album: Models.IAlbum; + alert: Models.IAlert; + artists: Array; + genres: Array; + save(); + clearAlert(); + } + + class AlbumEditController implements IAlbumDetailsViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _artistApi: ArtistApi.IArtistApiService; + private _genreApi: GenreApi.IGenreApiService; + private _viewAlert: ViewAlert.IViewAlertService; + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor($routeParams: IAlbumDetailsRouteParams, + albumApi: AlbumApi.IAlbumApiService, + artistApi: ArtistApi.IArtistApiService, + genreApi: GenreApi.IGenreApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + $timeout: ng.ITimeoutService, + $q: ng.IQService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._artistApi = artistApi; + this._genreApi = genreApi; + this._viewAlert = viewAlert; + this._modal = $modal; + this._location = $location; + this._timeout = $timeout; + this._log = $log; + + this.mode = $routeParams.mode; + + this.alert = viewAlert.alert; + + artistApi.getArtistsLookup().then(artists => this.artists = artists); + genreApi.getGenresLookup().then(genres => this.genres = genres); + + if (this.mode.toLowerCase() === "edit") { + // TODO: Handle album load failure + albumApi.getAlbumDetails($routeParams.albumId).then(album => { + this.album = album; + + // Pre-load the lookup arrays with the current values if not set yet + this.genres = this.genres || [album.Genre]; + this.artists = this.artists || [album.Artist]; + + this.disabled = false; + }); + } else { + this.disabled = false; + } + } + + public mode: string; + + public disabled = true; + + public album: Models.IAlbum; + + public alert: Models.IAlert; + + public artists: Array; + + public genres: Array; + + public save() { + this.disabled = true; + + var apiMethod = this.mode.toLowerCase() === "edit" ? this._albumApi.updateAlbum : this._albumApi.createAlbum; + apiMethod = apiMethod.bind(this._albumApi); + + apiMethod(this.album).then( + // Success + response => { + var alert = { + type: Models.AlertType.success, + message: response.data.Message + }; + + // TODO: Do we need to destroy this timeout on controller unload? + this._timeout(() => this.alert !== alert || this.clearAlert(), 3000); + + if (this.mode.toLowerCase() === "new") { + this._log.info("Created album successfully!"); + + var albumId: number = response.data.Data; + + this._viewAlert.alert = alert; + + // Reload the view with the new album ID + this._location.path("/albums/" + albumId + "/edit").replace(); + } else { + this.alert = alert; + this.disabled = false; + this._log.info("Updated album " + this.album.AlbumId + " successfully!"); + } + }, + // Error + response => { + // TODO: Make this common logic, e.g. base controller class, injected helper service, etc. + if (response.status === 400) { + // We made a bad request + if (response.data && response.data.ModelErrors) { + // The server says the update failed validation + // TODO: Map errors back to client validators and/or summary + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message, + modelErrors: response.data.ModelErrors + }; + this.disabled = false; + } else { + // Some other bad request, just show the message + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } + } else if (response.status === 404) { + // The album wasn't found, probably deleted. Leave the form disabled and show error message. + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } else if (response.status === 401) { + // We need to authenticate again + // TODO: Should we just redirect to login page, show a message with a link, or something else + this.alert = { + type: Models.AlertType.danger, + message: "Your session has timed out. Please log in and try again." + }; + } else if (!response.status) { + // Request timed out or no response from server or worse + this._log.error("Error updating album " + this.album.AlbumId); + this._log.error(response); + this.alert = { type: Models.AlertType.danger, message: "An unexpected error occurred. Please try again." }; + this.disabled = false; + } + }); + } + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml new file mode 100644 index 0000000000..14d945d6d2 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml @@ -0,0 +1,77 @@ +@model MvcMusicStore.Models.Album + +
+

Albums

+

+ Create new +

+ + + {{ viewModel.alert.message }} +
    +
  • {{ modelError.ErrorMessage }}
  • +
+
+ + + + + + + + + + + + + + + + + + + + +
+ @Html.DisplayNameFor(m => m.Genre) + + + + + @Html.DisplayNameFor(m => m.Artist) + + + + + @Html.DisplayNameFor(m => m.Title) + + + + + @Html.DisplayNameFor(m => m.Price) + + + +
+ {{ album.Genre.Name }} + + {{ album.Artist.Name | truncate:25 }} + + {{ album.Title | truncate:25 }} + + {{ album.Price | currency }} + +
+ Details + Edit + Delete +
+
+ +

+ {{ viewModel.totalCount }} total albums +

+
\ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ng.ts new file mode 100644 index 0000000000..11782f9c2e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ng.ts @@ -0,0 +1,135 @@ +/// + +module MusicStore.Admin.Catalog { + interface IAlbumListViewModel { + albums: Array; + totalCount: number; + currentPage: number; + pageSize: number; + loadPage(page?: number); + deleteAlbum(album: Models.IAlbum); + clearAlert(); + } + + class AlbumListController implements IAlbumListViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _modal: ng.ui.bootstrap.IModalService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor(albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $timeout: ng.ITimeoutService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._modal = $modal; + this._timeout = $timeout; + this._log = $log; + + this.currentPage = 1; + this.pageSize = 50; + this.loadPage(1); + this.sortColumn = "Title"; + + this.showAlert(viewAlert.alert, 3000); + viewAlert.alert = null; + } + + public alert: Models.IAlert; + + public albums: Array; + + public totalCount: number; + + public currentPage: number; + + public pageSize: number; + + public sortColumn: string; + + public sortDescending: boolean; + + public loadPage(page?: number) { + page = page || this.currentPage; + var sortByExpression = this.getSortByExpression(); + this._albumApi.getAlbums(page, this.pageSize, sortByExpression).then(result => { + this.albums = result.Data; + this.currentPage = result.Page; + this.totalCount = result.TotalCount; + }); + } + + public sortBy(column: string) { + if (this.sortColumn === column) { + // Just flip the direction + this.sortDescending = !this.sortDescending; + } else { + this.sortColumn = column; + this.sortDescending = false; + } + + this.loadPage(); + } + + public deleteAlbum(album: Models.IAlbum) { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(album.AlbumId).then(result => { + this.loadPage(); + + this.showAlert({ + type: Models.AlertType.success, + message: result.data.Message + }, 3000); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + + private showAlert(alert: Models.IAlert, closeAfter?: number) { + if (!alert) { + return; + } + + this.alert = alert; + + // TODO: Do we need to destroy this timeout on controller unload? + if (closeAfter) { + this._timeout(() => this.alert !== alert || this.clearAlert(), closeAfter); + } + } + + private getSortByExpression() { + if (this.sortDescending) { + return this.sortColumn + " DESC"; + } + return this.sortColumn; + } + } + + angular.module("MusicStore.Admin.Catalog") + .controller("MusicStore.Admin.Catalog.AlbumListController", [ + "MusicStore.AlbumApi.IAlbumApiService", + "MusicStore.ViewAlert.IViewAlertService", + "$modal", + "$timeout", + "$log", + AlbumListController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts new file mode 100644 index 0000000000..420e32c4c9 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts @@ -0,0 +1,123 @@ +module MusicStore.Admin.Catalog { + interface IAlbumListViewModel { + albums: Array; + totalCount: number; + currentPage: number; + pageSize: number; + loadPage(page?: number); + deleteAlbum(album: Models.IAlbum); + clearAlert(); + } + + class AlbumListController implements IAlbumListViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _modal: ng.ui.bootstrap.IModalService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor(albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $timeout: ng.ITimeoutService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._modal = $modal; + this._timeout = $timeout; + this._log = $log; + + this.currentPage = 1; + this.pageSize = 50; + this.loadPage(1); + this.sortColumn = "Title"; + + this.showAlert(viewAlert.alert, 3000); + viewAlert.alert = null; + } + + public alert: Models.IAlert; + + public albums: Array; + + public totalCount: number; + + public currentPage: number; + + public pageSize: number; + + public sortColumn: string; + + public sortDescending: boolean; + + public loadPage(page?: number) { + page = page || this.currentPage; + var sortByExpression = this.getSortByExpression(); + this._albumApi.getAlbums(page, this.pageSize, sortByExpression).then(result => { + this.albums = result.Data; + this.currentPage = result.Page; + this.totalCount = result.TotalCount; + }); + } + + public sortBy(column: string) { + if (this.sortColumn === column) { + // Just flip the direction + this.sortDescending = !this.sortDescending; + } else { + this.sortColumn = column; + this.sortDescending = false; + } + + this.loadPage(); + } + + public deleteAlbum(album: Models.IAlbum) { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(album.AlbumId).then(result => { + this.loadPage(); + + this.showAlert({ + type: Models.AlertType.success, + message: result.data.Message + }, 3000); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + + private showAlert(alert: Models.IAlert, closeAfter?: number) { + if (!alert) { + return; + } + + this.alert = alert; + + // TODO: Do we need to destroy this timeout on controller unload? + if (closeAfter) { + this._timeout(() => this.alert !== alert || this.clearAlert(), closeAfter); + } + } + + private getSortByExpression() { + if (this.sortDescending) { + return this.sortColumn + " DESC"; + } + return this.sortColumn; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ng.ts new file mode 100644 index 0000000000..c5fcd4fd91 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ng.ts @@ -0,0 +1,65 @@ +/// + +module MusicStore.Admin { + angular.module("MusicStore.Admin", [ + "ngRoute", + "ui.bootstrap", + "MusicStore.InlineData", + "MusicStore.GenreMenu", + "MusicStore.UrlResolver", + "MusicStore.UserDetails", + "MusicStore.LoginLink", + "MusicStore.Visited", + "MusicStore.TitleCase", + "MusicStore.Truncate", + "MusicStore.GenreApi", + "MusicStore.AlbumApi", + "MusicStore.ArtistApi", + "MusicStore.ViewAlert", + "MusicStore.Admin.Catalog", + ]).config([ + "$routeProvider", + "$logProvider", + configuration + ]); + + + var dependencies = [ + "ngRoute", + "ui.bootstrap", + MusicStore.InlineData, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.Visited, + MusicStore.TitleCase, + MusicStore.Truncate, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.ArtistApi, + MusicStore.ViewAlert, + MusicStore.Admin.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + // Configure routes + $routeProvider + .when("/albums/:albumId/details", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml" }) + .when("/albums/:albumId/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml" }) + .otherwise({ redirectTo: "/albums" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + //function BUG:run() { + + //} +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts new file mode 100644 index 0000000000..ab415980c8 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts @@ -0,0 +1,43 @@ +/// + +module MusicStore.Admin { + + var dependencies = [ + "ngRoute", + "ui.bootstrap", + MusicStore.InlineData, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.Visited, + MusicStore.TitleCase, + MusicStore.Truncate, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.ArtistApi, + MusicStore.ViewAlert, + MusicStore.Admin.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + // Configure routes + $routeProvider + .when("/albums/:albumId/details", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml" }) + .when("/albums/:albumId/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml" }) + .otherwise({ redirectTo: "/albums" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + //function BUG:run() { + + //} +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.AlbumApi.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.AlbumApi.ng.ts new file mode 100644 index 0000000000..1a800213a5 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.AlbumApi.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.AlbumApi { + angular.module("MusicStore.AlbumApi", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ArtistApi.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ArtistApi.ng.ts new file mode 100644 index 0000000000..a9244719dc --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ArtistApi.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.ArtistApi { + angular.module("MusicStore.ArtistApi", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreApi.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreApi.ng.ts new file mode 100644 index 0000000000..26892bd0eb --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreApi.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.GenreApi { + angular.module("MusicStore.GenreApi", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreMenu.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreMenu.ng.ts new file mode 100644 index 0000000000..22668b031b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.GenreMenu.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.GenreMenu { + angular.module("MusicStore.GenreMenu", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.InlineData.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.InlineData.ng.ts new file mode 100644 index 0000000000..36bb08aa4b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.InlineData.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.InlineData { + angular.module("MusicStore.InlineData", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.LoginLink.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.LoginLink.ng.ts new file mode 100644 index 0000000000..7e8531d163 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.LoginLink.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.LoginLink { + angular.module("MusicStore.LoginLink", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.PreventSubmit.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.PreventSubmit.ng.ts new file mode 100644 index 0000000000..d45d0a89c3 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.PreventSubmit.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.PreventSubmit { + angular.module("MusicStore.PreventSubmit", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Catalog.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Catalog.ng.ts new file mode 100644 index 0000000000..fe4813215c --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Catalog.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.Store.Catalog { + angular.module("MusicStore.Store.Catalog", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Home.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Home.ng.ts new file mode 100644 index 0000000000..8df65c47a4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store.Home.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.Store.Home { + angular.module("MusicStore.Store.Home", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html new file mode 100644 index 0000000000..1a5be7674f --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html @@ -0,0 +1,26 @@ +
+

{{ viewModel.album.Title }}

+ +

+ +

+ +
+

+ Genre: + {{ viewModel.album.Genre.Name }} +

+

+ Artist: + {{ viewModel.album.Artist.Name }} +

+

+ Price: + {{ viewModel.album.Price | currency }} +

+

+ + Add to cart +

+
+
\ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ng.ts new file mode 100644 index 0000000000..966e98ebbc --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ng.ts @@ -0,0 +1,31 @@ +/// + +module MusicStore.Store.Catalog { + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + } + + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + public album: Models.IAlbum; + + constructor($routeParams: IAlbumDetailsRouteParams, albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this, + albumId = $routeParams.albumId; + + albumApi.getAlbumDetails(albumId).then(album => { + viewModel.album = album; + }); + } + } + + angular.module("MusicStore.Store.Catalog") + .controller("MusicStore.Store.Catalog.AlbumDetailsController", [ + "$routeParams", + "MusicStore.AlbumApi.IAlbumApiService", + AlbumDetailsController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts new file mode 100644 index 0000000000..4f9c52f651 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts @@ -0,0 +1,22 @@ +module MusicStore.Store.Catalog { + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + } + + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + public album: Models.IAlbum; + + constructor($routeParams: IAlbumDetailsRouteParams, albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this, + albumId = $routeParams.albumId; + + albumApi.getAlbumDetails(albumId).then(album => { + viewModel.album = album; + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html new file mode 100644 index 0000000000..203d2e4164 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html @@ -0,0 +1,12 @@ +
+

{{ viewModel.genre.Name }} Albums

+ + +
\ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ng.ts new file mode 100644 index 0000000000..7ed563cee5 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ng.ts @@ -0,0 +1,30 @@ +/// + +module MusicStore.Store.Catalog { + interface IGenreDetailsViewModel { + albums: Array; + } + + interface IGenreDetailsRouteParams extends ng.route.IRouteParamsService { + genreId: number; + } + + class GenreDetailsController implements IGenreDetailsViewModel { + public albums: Array; + + constructor($routeParams: IGenreDetailsRouteParams, genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenreAlbums($routeParams.genreId).success(result => { + viewModel.albums = result; + }); + } + } + + angular.module("MusicStore.Store.Catalog") + .controller("MusicStore.Store.Catalog.GenreDetailsController", [ + "$routeParams", + "MusicStore.GenreApi.IGenreApiService", + GenreDetailsController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts new file mode 100644 index 0000000000..f31d158e9a --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts @@ -0,0 +1,21 @@ +module MusicStore.Store.Catalog { + interface IGenreDetailsViewModel { + albums: Array; + } + + interface IGenreDetailsRouteParams extends ng.route.IRouteParamsService { + genreId: number; + } + + class GenreDetailsController implements IGenreDetailsViewModel { + public albums: Array; + + constructor($routeParams: IGenreDetailsRouteParams, genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenreAlbums($routeParams.genreId).success(result => { + viewModel.albums = result; + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html new file mode 100644 index 0000000000..0c78a4bb60 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html @@ -0,0 +1,12 @@ +
+

Browse Genres

+ +

+ Select from {{ viewModel.genres.length }} genres: +

+ +
\ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ng.ts new file mode 100644 index 0000000000..9429e353e3 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ng.ts @@ -0,0 +1,25 @@ +/// + +module MusicStore.Store.Catalog { + interface IGenreListViewModel { + genres: Array; + } + + class GenreListController implements IGenreListViewModel { + public genres: Array; + + constructor(genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenresList().success(function (genres) { + viewModel.genres = genres; + }); + } + } + + angular.module("MusicStore.Store.Catalog") + .controller("MusicStore.Store.Catalog.GenreListController", [ + "MusicStore.GenreApi.IGenreApiService", + GenreListController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts new file mode 100644 index 0000000000..1f187cf204 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts @@ -0,0 +1,17 @@ +module MusicStore.Store.Catalog { + interface IGenreListViewModel { + genres: Array; + } + + class GenreListController implements IGenreListViewModel { + public genres: Array; + + constructor(genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenresList().success(function (genres) { + viewModel.genres = genres; + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html new file mode 100644 index 0000000000..ca35168092 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html @@ -0,0 +1,15 @@ +
+

MVC Music Store

+ +
+ + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ng.ts new file mode 100644 index 0000000000..81fb1f81c7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ng.ts @@ -0,0 +1,25 @@ +/// + +module MusicStore.Store.Home { + interface IHomeViewModel { + albums: Array + } + + class HomeController implements IHomeViewModel { + public albums: Array; + + constructor(albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this; + + albumApi.getMostPopularAlbums().then(albums => { + viewModel.albums = albums; + }); + } + } + + angular.module("MusicStore.Store.Home") + .controller("MusicStore.Store.Home.HomeController", [ + "MusicStore.AlbumApi.IAlbumApiService", + HomeController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts new file mode 100644 index 0000000000..fb17e77da1 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts @@ -0,0 +1,17 @@ +module MusicStore.Store.Home { + interface IHomeViewModel { + albums: Array + } + + class HomeController implements IHomeViewModel { + public albums: Array; + + constructor(albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this; + + albumApi.getMostPopularAlbums().then(albums => { + viewModel.albums = albums; + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ng.ts new file mode 100644 index 0000000000..eb5f232456 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ng.ts @@ -0,0 +1,60 @@ +/// + +module MusicStore.Store { + angular.module("MusicStore.Store", [ + "ngRoute", + "MusicStore.InlineData", + "MusicStore.PreventSubmit", + "MusicStore.GenreMenu", + "MusicStore.UrlResolver", + "MusicStore.UserDetails", + "MusicStore.LoginLink", + "MusicStore.GenreApi", + "MusicStore.AlbumApi", + "MusicStore.Store.Home", + "MusicStore.Store.Catalog", + ]).config([ + "$routeProvider", + "$logProvider", + configuration + ]).run([ + "$log", + "MusicStore.UserDetails.IUserDetailsService", + run + ]); + + + var dependencies = [ + "ngRoute", + MusicStore.InlineData, + MusicStore.PreventSubmit, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.Store.Home, + MusicStore.Store.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + $routeProvider + .when("/", { templateUrl: "ng-apps/MusicStore.Store/Home/Home.html" }) + .when("/albums/genres", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreList.html" }) + .when("/albums/genres/:genreId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreDetails.html" }) + .when("/albums/:albumId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/AlbumDetails.html" }) + .otherwise({ redirectTo: "/" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + function run($log: ng.ILogService, userDetails: UserDetails.IUserDetailsService) { + $log.log(userDetails.getUserDetails()); + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts new file mode 100644 index 0000000000..f679cc2d3b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts @@ -0,0 +1,38 @@ +/// + +module MusicStore.Store { + + var dependencies = [ + "ngRoute", + MusicStore.InlineData, + MusicStore.PreventSubmit, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.Store.Home, + MusicStore.Store.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + $routeProvider + .when("/", { templateUrl: "ng-apps/MusicStore.Store/Home/Home.html" }) + .when("/albums/genres", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreList.html" }) + .when("/albums/genres/:genreId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreDetails.html" }) + .when("/albums/:albumId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/AlbumDetails.html" }) + .otherwise({ redirectTo: "/" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + function run($log: ng.ILogService, userDetails: UserDetails.IUserDetailsService) { + $log.log(userDetails.getUserDetails()); + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.TitleCase.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.TitleCase.ng.ts new file mode 100644 index 0000000000..1ac0a57f1f --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.TitleCase.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.TitleCase { + angular.module("MusicStore.TitleCase", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Truncate.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Truncate.ng.ts new file mode 100644 index 0000000000..93446dd698 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Truncate.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.Truncate { + angular.module("MusicStore.Truncate", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UrlResolver.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UrlResolver.ng.ts new file mode 100644 index 0000000000..b3388c5348 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UrlResolver.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.UrlResolver { + angular.module("MusicStore.UrlResolver", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UserDetails.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UserDetails.ng.ts new file mode 100644 index 0000000000..84a96e6c73 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.UserDetails.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.UserDetails { + angular.module("MusicStore.UserDetails", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ViewAlert.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ViewAlert.ng.ts new file mode 100644 index 0000000000..c7f5ed3bda --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.ViewAlert.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.ViewAlert { + angular.module("MusicStore.ViewAlert", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Visited.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Visited.ng.ts new file mode 100644 index 0000000000..6300a1e26f --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/MusicStore.Visited.ng.ts @@ -0,0 +1,3 @@ +module MusicStore.Visited { + angular.module("MusicStore.Visited", []); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/Web.config b/src/MvcMusicStore.Spa/Client/ng-apps/Web.config new file mode 100644 index 0000000000..b3ccf45399 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/Web.config @@ -0,0 +1,33 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ng.ts new file mode 100644 index 0000000000..24d65aee67 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ng.ts @@ -0,0 +1,109 @@ +/// + +module MusicStore.AlbumApi { + export interface IAlbumApiService { + getAlbums(page?: number, pageSize?: number, sortBy?: string): ng.IPromise>; + getAlbumDetails(albumId: number): ng.IPromise; + getMostPopularAlbums(count?: number): ng.IPromise>; + createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + deleteAlbum(albumId: number, config?: ng.IRequestConfig): ng.IHttpPromise; + } + + class AlbumApiService implements IAlbumApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getAlbums(page?: number, pageSize?: number, sortBy?: string) { + var url = this._urlResolver.resolveUrl("~/api/albums"), + query: any = {}, + querySeparator = "?", + inlineData; + + if (page) { + query.page = page; + } + + if (pageSize) { + query.pageSize = pageSize; + } + + if (sortBy) { + query.sortBy = sortBy; + } + + for (var key in query) { + if (query.hasOwnProperty(key)) { + url += querySeparator + key + "=" + encodeURIComponent(query[key]); + if (querySeparator === "?") { + querySeparator = "&"; + } + } + } + + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getAlbumDetails(albumId: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/" + albumId); + return this._http.get(url).then(result => result.data); + } + + public getMostPopularAlbums(count?: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/mostPopular"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + if (count && count > 0) { + url += "?count=" + count; + } + + return this._http.get(url).then(result => result.data); + } + } + + public createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums"); + return this._http.post(url, album, config || { timeout: 10000 }); + } + + public updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + album.AlbumId + "/update"); + return this._http.put(url, album, config || { timeout: 10000 }); + } + + public deleteAlbum(albumId: number, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + albumId); + return this._http.delete(url, config || { timeout: 10000 }); + } + } + + angular.module("MusicStore.AlbumApi") + .service("MusicStore.AlbumApi.IAlbumApiService", [ + "$cacheFactory", + "$q", + "$http", + "MusicStore.UrlResolver.IUrlResolverService", + AlbumApiService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts new file mode 100644 index 0000000000..d68bfd607d --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts @@ -0,0 +1,98 @@ +module MusicStore.AlbumApi { + export interface IAlbumApiService { + getAlbums(page?: number, pageSize?: number, sortBy?: string): ng.IPromise>; + getAlbumDetails(albumId: number): ng.IPromise; + getMostPopularAlbums(count?: number): ng.IPromise>; + createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + deleteAlbum(albumId: number, config?: ng.IRequestConfig): ng.IHttpPromise; + } + + class AlbumApiService implements IAlbumApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getAlbums(page?: number, pageSize?: number, sortBy?: string) { + var url = this._urlResolver.resolveUrl("~/api/albums"), + query: any = {}, + querySeparator = "?", + inlineData; + + if (page) { + query.page = page; + } + + if (pageSize) { + query.pageSize = pageSize; + } + + if (sortBy) { + query.sortBy = sortBy; + } + + for (var key in query) { + if (query.hasOwnProperty(key)) { + url += querySeparator + key + "=" + encodeURIComponent(query[key]); + if (querySeparator === "?") { + querySeparator = "&"; + } + } + } + + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getAlbumDetails(albumId: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/" + albumId); + return this._http.get(url).then(result => result.data); + } + + public getMostPopularAlbums(count?: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/mostPopular"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + if (count && count > 0) { + url += "?count=" + count; + } + + return this._http.get(url).then(result => result.data); + } + } + + public createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums"); + return this._http.post(url, album, config || { timeout: 10000 }); + } + + public updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + album.AlbumId + "/update"); + return this._http.put(url, album, config || { timeout: 10000 }); + } + + public deleteAlbum(albumId: number, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + albumId); + return this._http.delete(url, config || { timeout: 10000 }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ng.ts new file mode 100644 index 0000000000..e72e91c031 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ng.ts @@ -0,0 +1,44 @@ +/// + +module MusicStore.ArtistApi { + export interface IArtistApiService { + getArtistsLookup(): ng.IPromise>; + } + + class ArtistsApiService implements IArtistApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getArtistsLookup() { + var url = this._urlResolver.resolveUrl("~/api/artists/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + } + + angular.module("MusicStore.ArtistApi") + .service("MusicStore.ArtistApi.IArtistApiService", [ + "$cacheFactory", + "$q", + "$http", + "MusicStore.UrlResolver.IUrlResolverService", + ArtistsApiService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts new file mode 100644 index 0000000000..45a59f3563 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts @@ -0,0 +1,33 @@ +module MusicStore.ArtistApi { + export interface IArtistApiService { + getArtistsLookup(): ng.IPromise>; + } + + class ArtistsApiService implements IArtistApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getArtistsLookup() { + var url = this._urlResolver.resolveUrl("~/api/artists/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ng.ts new file mode 100644 index 0000000000..67b99789ba --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ng.ts @@ -0,0 +1,68 @@ +/// + +module MusicStore.GenreApi { + export interface IGenreApiService { + getGenresLookup(): ng.IPromise>; + getGenresMenu(): ng.IPromise>; + getGenresList(): ng.IHttpPromise>; + getGenreAlbums(genreId: number): ng.IHttpPromise>; + } + + class GenreApiService implements IGenreApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getGenresLookup() { + var url = this._urlResolver.resolveUrl("~/api/genres/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresMenu() { + var url = this._urlResolver.resolveUrl("~/api/genres/menu"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresList() { + var url = this._urlResolver.resolveUrl("~/api/genres"); + return this._http.get(url); + } + + public getGenreAlbums(genreId: number) { + var url = this._urlResolver.resolveUrl("~/api/genres/" + genreId + "/albums"); + return this._http.get(url); + } + } + + angular.module("MusicStore.GenreApi") + .service("MusicStore.GenreApi.IGenreApiService", [ + "$cacheFactory", + "$q", + "$http", + "MusicStore.UrlResolver.IUrlResolverService", + GenreApiService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts new file mode 100644 index 0000000000..b92d794161 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts @@ -0,0 +1,57 @@ +module MusicStore.GenreApi { + export interface IGenreApiService { + getGenresLookup(): ng.IPromise>; + getGenresMenu(): ng.IPromise>; + getGenresList(): ng.IHttpPromise>; + getGenreAlbums(genreId: number): ng.IHttpPromise>; + } + + class GenreApiService implements IGenreApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getGenresLookup() { + var url = this._urlResolver.resolveUrl("~/api/genres/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresMenu() { + var url = this._urlResolver.resolveUrl("~/api/genres/menu"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresList() { + var url = this._urlResolver.resolveUrl("~/api/genres"); + return this._http.get(url); + } + + public getGenreAlbums(genreId: number) { + var url = this._urlResolver.resolveUrl("~/api/genres/" + genreId + "/albums"); + return this._http.get(url); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html new file mode 100644 index 0000000000..888ae81db7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ng.ts new file mode 100644 index 0000000000..872e009a3e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ng.ts @@ -0,0 +1,31 @@ +/// + +module MusicStore.GenreMenu { + interface IGenreMenuViewModel { + genres: Array; + urlBase: string; + } + + class GenreMenuController implements IGenreMenuViewModel { + constructor(genreApi: GenreApi.IGenreApiService, urlResolver: UrlResolver.IUrlResolverService) { + var viewModel = this; + + genreApi.getGenresMenu().then(genres => { + viewModel.genres = genres; + }); + + viewModel.urlBase = urlResolver.base; + } + + public genres: Array; + + public urlBase: string; + } + + angular.module("MusicStore.GenreMenu") + .controller("MusicStore.GenreMenu.GenreMenuController", [ + "MusicStore.GenreApi.IGenreApiService", + "MusicStore.UrlResolver.IUrlResolverService", + GenreMenuController + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts new file mode 100644 index 0000000000..2ccfb0f8b1 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts @@ -0,0 +1,22 @@ +module MusicStore.GenreMenu { + interface IGenreMenuViewModel { + genres: Array; + urlBase: string; + } + + class GenreMenuController implements IGenreMenuViewModel { + constructor(genreApi: GenreApi.IGenreApiService, urlResolver: UrlResolver.IUrlResolverService) { + var viewModel = this; + + genreApi.getGenresMenu().then(genres => { + viewModel.genres = genres; + }); + + viewModel.urlBase = urlResolver.base; + } + + public genres: Array; + + public urlBase: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ng.ts new file mode 100644 index 0000000000..516abdb7dd --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ng.ts @@ -0,0 +1,28 @@ +/// + +module MusicStore.GenreMenu { + + //@NgDirective('appGenreMenu') + class GenreMenuDirective implements ng.IDirective { + public replace = true; + public restrict = "A"; + public templateUrl; + + constructor(urlResolver: UrlResolver.IUrlResolverService) { + for (var m in this) { + if (this[m].bind) { + this[m] = this[m].bind(this); + } + } + this.templateUrl = urlResolver.resolveUrl("~/ng-apps/components/GenreMenu/GenreMenu.html"); + } + } + + angular.module("MusicStore.GenreMenu") + .directive("appGenreMenu", [ + "MusicStore.UrlResolver.IUrlResolverService", + function (a) { + return new GenreMenuDirective(a); + } + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts new file mode 100644 index 0000000000..47f9ed2691 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts @@ -0,0 +1,13 @@ +module MusicStore.GenreMenu { + + //@NgDirective('appGenreMenu') + class GenreMenuDirective implements ng.IDirective { + public replace = true; + public restrict = "A"; + public templateUrl; + + constructor(urlResolver: UrlResolver.IUrlResolverService) { + this.templateUrl = urlResolver.resolveUrl("~/ng-apps/components/GenreMenu/GenreMenu.html"); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ng.ts new file mode 100644 index 0000000000..419118a231 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ng.ts @@ -0,0 +1,47 @@ +/// + +module MusicStore.InlineData { + interface InlineDataAttributes extends ng.IAttributes { + type: string; + for: string; + } + + //@NgDirective('appInlineData') + class InlineDataDirective implements ng.IDirective { + private _cache: ng.ICacheObject; + private _log: ng.ILogService; + + constructor($cacheFactory: ng.ICacheFactoryService, $log: ng.ILogService) { + for (var m in this) { + if (this[m].bind) { + this[m] = this[m].bind(this); + } + } + this._cache = $cacheFactory.get("inlineData") || $cacheFactory("inlineData"); + this._log = $log; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: InlineDataAttributes) { + var data = attrs.type === "application/json" + ? angular.fromJson(element.text()) + : element.text(); + + this._log.info("appInlineData: Inline data element found for " + attrs.for); + + this._cache.put(attrs.for, data); + + //element.remove(); + } + } + + angular.module("MusicStore.InlineData") + .directive("appInlineData", [ + "$cacheFactory", + "$log", + function (a,b) { + return new InlineDataDirective(a,b); + } + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts new file mode 100644 index 0000000000..ade0a68c1d --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts @@ -0,0 +1,31 @@ +module MusicStore.InlineData { + interface InlineDataAttributes extends ng.IAttributes { + type: string; + for: string; + } + + //@NgDirective('appInlineData') + class InlineDataDirective implements ng.IDirective { + private _cache: ng.ICacheObject; + private _log: ng.ILogService; + + constructor($cacheFactory: ng.ICacheFactoryService, $log: ng.ILogService) { + this._cache = $cacheFactory.get("inlineData") || $cacheFactory("inlineData"); + this._log = $log; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: InlineDataAttributes) { + var data = attrs.type === "application/json" + ? angular.fromJson(element.text()) + : element.text(); + + this._log.info("appInlineData: Inline data element found for " + attrs.for); + + this._cache.put(attrs.for, data); + + //element.remove(); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ng.ts new file mode 100644 index 0000000000..cbe2edc2bb --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ng.ts @@ -0,0 +1,49 @@ +/// + +module MusicStore.LoginLink { + interface LoginLinkAttributes extends ng.IAttributes { + href: string; + } + + //@NgDirective('appLoginLink') + class LoginLinkDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor(urlResolver: UrlResolver.IUrlResolverService, $window: ng.IWindowService) { + for (var m in this) { + if (this[m].bind) { + this[m] = this[m].bind(this); + } + } + this._window = $window; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: LoginLinkAttributes) { + if (!element.is("a[href]")) { + return; + } + + // Grab the original login URL + var loginUrl = attrs.href; + + element.click(event => { + // Update the returnUrl querystring value to current path + var currentUrl = this._window.location.pathname + this._window.location.search + this._window.location.hash, + newUrl = loginUrl + "?returnUrl=" + encodeURIComponent(currentUrl); + + element.prop("href", newUrl); + }); + } + } + + angular.module("MusicStore.LoginLink") + .directive("appLoginLink", [ + "MusicStore.UrlResolver.IUrlResolverService", + "$window", + function (a,b) { + return new LoginLinkDirective(a,b); + } + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts new file mode 100644 index 0000000000..051f1acd42 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts @@ -0,0 +1,33 @@ +module MusicStore.LoginLink { + interface LoginLinkAttributes extends ng.IAttributes { + href: string; + } + + //@NgDirective('appLoginLink') + class LoginLinkDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor(urlResolver: UrlResolver.IUrlResolverService, $window: ng.IWindowService) { + this._window = $window; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: LoginLinkAttributes) { + if (!element.is("a[href]")) { + return; + } + + // Grab the original login URL + var loginUrl = attrs.href; + + element.click(event => { + // Update the returnUrl querystring value to current path + var currentUrl = this._window.location.pathname + this._window.location.search + this._window.location.hash, + newUrl = loginUrl + "?returnUrl=" + encodeURIComponent(currentUrl); + + element.prop("href", newUrl); + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ng.ts new file mode 100644 index 0000000000..fd8bce8f62 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ng.ts @@ -0,0 +1,16 @@ +module MusicStore.Models { + export interface IAlbum { + AlbumId: number; + GenreId: number; + ArtistId: number; + + Title: string; + AlbumArtUrl: string; + Price: number; + + Artist: IArtist; + Genre: IGenre; + + DetailsUrl: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts new file mode 100644 index 0000000000..fd8bce8f62 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts @@ -0,0 +1,16 @@ +module MusicStore.Models { + export interface IAlbum { + AlbumId: number; + GenreId: number; + ArtistId: number; + + Title: string; + AlbumArtUrl: string; + Price: number; + + Artist: IArtist; + Genre: IGenre; + + DetailsUrl: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ng.ts new file mode 100644 index 0000000000..5184e73fdf --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ng.ts @@ -0,0 +1,25 @@ +module MusicStore.Models { + export interface IAlert { + type: AlertType; + message: string; + } + + export interface IModelErrorAlert extends IAlert { + modelErrors: Array; + } + + export class AlertType { + constructor(public value: string) { + } + + public toString() { + return this.value; + } + + // Values + static success = new AlertType("success"); + static info = new AlertType("info"); + static warning = new AlertType("warning"); + static danger = new AlertType("danger"); + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts new file mode 100644 index 0000000000..f3f3578ab2 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts @@ -0,0 +1,25 @@ +module MusicStore.Models { + export interface IAlert { + type: AlertType; + message: string; + } + + export interface IModelErrorAlert extends IAlert { + modelErrors: Array; + } + + export class AlertType { + constructor(public value: string) { + } + + public toString() { + return this.value; + } + + // Values + static success = new AlertType("success"); + static info = new AlertType("info"); + static warning = new AlertType("warning"); + static danger = new AlertType("danger"); + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ng.ts new file mode 100644 index 0000000000..6ccc2fd98e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ng.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IApiResult { + Message?: string; + Data?: any; + ModelErrors?: Array; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts new file mode 100644 index 0000000000..c69a3ca54b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IApiResult { + Message?: string; + Data?: any; + ModelErrors?: Array; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ng.ts new file mode 100644 index 0000000000..9d40978f30 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ng.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IArtist { + ArtistId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts new file mode 100644 index 0000000000..9d40978f30 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IArtist { + ArtistId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ng.ts new file mode 100644 index 0000000000..6b60385fc7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ng.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IGenre { + GenreId: number; + Name: string; + Description: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts new file mode 100644 index 0000000000..6b60385fc7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IGenre { + GenreId: number; + Name: string; + Description: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ng.ts new file mode 100644 index 0000000000..1cc50c8e30 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ng.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IGenreLookup { + GenreId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts new file mode 100644 index 0000000000..1cc50c8e30 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IGenreLookup { + GenreId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ng.ts new file mode 100644 index 0000000000..8d062d945d --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ng.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IModelError { + FieldName: string; + ErrorMessage: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts new file mode 100644 index 0000000000..294ceeb80e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IModelError { + FieldName: string; + ErrorMessage: string; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ng.ts new file mode 100644 index 0000000000..56b7efa324 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ng.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IPagedList { + Data: Array; + Page: number; + PageSize: number; + TotalCount: number; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts new file mode 100644 index 0000000000..e109a6ec1f --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IPagedList { + Data: Array; + Page: number; + PageSize: number; + TotalCount: number; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ng.ts new file mode 100644 index 0000000000..ddac9992f4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ng.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IUserDetails { + isAuthenticated: boolean; + userName: string; + userId: string; + roles: Array; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts new file mode 100644 index 0000000000..ddac9992f4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IUserDetails { + isAuthenticated: boolean; + userName: string; + userId: string; + roles: Array; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ng.ts new file mode 100644 index 0000000000..b330c2d11d --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ng.ts @@ -0,0 +1,41 @@ +/// + +module MusicStore.PreventSubmit { + interface IPreventSubmitAttributes extends ng.IAttributes { + name: string; + appPreventSubmit: string; + } + + //@NgDirective('appPreventSubmit') + class PreventSubmitDirective implements ng.IDirective { + constructor() { + for (var m in this) { + if (this[m].bind) { + this[m] = this[m].bind(this); + } + } + } + + private _preventSubmit: any; + + public restrict = "A"; + + public link(scope: any, element: ng.IAugmentedJQuery, attrs: IPreventSubmitAttributes) { + // TODO: Just make this directive apply to all
tags and no-op if no action attr + + element.submit(e => { + if (scope.$eval(attrs.appPreventSubmit)) { + e.preventDefault(); + return false; + } + }); + } + } + + angular.module("MusicStore.PreventSubmit") + .directive("appPreventSubmit", [ + function () { + return new PreventSubmitDirective(); + } + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts new file mode 100644 index 0000000000..e2d3423de2 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts @@ -0,0 +1,24 @@ +module MusicStore.PreventSubmit { + interface IPreventSubmitAttributes extends ng.IAttributes { + name: string; + appPreventSubmit: string; + } + + //@NgDirective('appPreventSubmit') + class PreventSubmitDirective implements ng.IDirective { + private _preventSubmit: any; + + public restrict = "A"; + + public link(scope: any, element: ng.IAugmentedJQuery, attrs: IPreventSubmitAttributes) { + // TODO: Just make this directive apply to all tags and no-op if no action attr + + element.submit(e => { + if (scope.$eval(attrs.appPreventSubmit)) { + e.preventDefault(); + return false; + } + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ng.ts new file mode 100644 index 0000000000..c65f449239 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ng.ts @@ -0,0 +1,23 @@ +/// + +module MusicStore.TitleCase { + + //@NgFilter('titlecase') + function titleCase(input: string) { + var out = "", + lastChar = ""; + + for (var i = 0; i < input.length; i++) { + out = out + (lastChar === " " || lastChar === "" + ? input.charAt(i).toUpperCase() + : input.charAt(i)); + + lastChar = input.charAt(i); + } + + return out; + } + + angular.module("MusicStore.TitleCase") + .filter("titlecase", () => titleCase); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts new file mode 100644 index 0000000000..65d9832c0e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts @@ -0,0 +1,18 @@ +module MusicStore.TitleCase { + + //@NgFilter('titlecase') + function titleCase(input: string) { + var out = "", + lastChar = ""; + + for (var i = 0; i < input.length; i++) { + out = out + (lastChar === " " || lastChar === "" + ? input.charAt(i).toUpperCase() + : input.charAt(i)); + + lastChar = input.charAt(i); + } + + return out; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ng.ts new file mode 100644 index 0000000000..49c982dd43 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ng.ts @@ -0,0 +1,20 @@ +/// + +module MusicStore.Truncate { + + //@NgFilter + function truncate(input: string, length: number) { + if (!input) { + return input; + } + + if (input.length <= length) { + return input; + } else { + return input.substr(0, length).trim() + "…"; + } + } + + angular.module("MusicStore.Truncate") + .filter("truncate", () => truncate); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts new file mode 100644 index 0000000000..554631858e --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts @@ -0,0 +1,15 @@ +module MusicStore.Truncate { + + //@NgFilter + function truncate(input: string, length: number) { + if (!input) { + return input; + } + + if (input.length <= length) { + return input; + } else { + return input.substr(0, length).trim() + "…"; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ng.ts new file mode 100644 index 0000000000..d6a2c051c5 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ng.ts @@ -0,0 +1,47 @@ +/// + +module MusicStore.UrlResolver { + export interface IUrlResolverService { + base: string; + resolveUrl(relativeUrl: string); + } + + class UrlResolverService implements IUrlResolverService { + private _base: string; + + constructor($rootElement: ng.IAugmentedJQuery) { + this._base = $rootElement.attr("data-url-base"); + + // Add trailing slash if not present + if (this._base === "" || this._base.substr(this._base.length - 1) !== "/") { + this._base = this._base + "/"; + } + } + + public get base() { + return this._base; + } + + public resolveUrl(relativeUrl: string) { + var firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "~") { + relativeUrl = relativeUrl.substr(1); + } + + firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "/") { + relativeUrl = relativeUrl.substr(1); + } + + return this._base + relativeUrl; + } + } + + angular.module("MusicStore.UrlResolver") + .service("MusicStore.UrlResolver.IUrlResolverService", [ + "$rootElement", + UrlResolverService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts new file mode 100644 index 0000000000..fd47021825 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts @@ -0,0 +1,39 @@ +module MusicStore.UrlResolver { + export interface IUrlResolverService { + base: string; + resolveUrl(relativeUrl: string); + } + + class UrlResolverService implements IUrlResolverService { + private _base: string; + + constructor($rootElement: ng.IAugmentedJQuery) { + this._base = $rootElement.attr("data-url-base"); + + // Add trailing slash if not present + if (this._base === "" || this._base.substr(this._base.length - 1) !== "/") { + this._base = this._base + "/"; + } + } + + public get base() { + return this._base; + } + + public resolveUrl(relativeUrl: string) { + var firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "~") { + relativeUrl = relativeUrl.substr(1); + } + + firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "/") { + relativeUrl = relativeUrl.substr(1); + } + + return this._base + relativeUrl; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ng.ts new file mode 100644 index 0000000000..57aa077a56 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ng.ts @@ -0,0 +1,42 @@ +/// + +module MusicStore.UserDetails { + export interface IUserDetailsService { + getUserDetails(): Models.IUserDetails; + getUserDetails(elementId: string): Models.IUserDetails; + } + + class UserDetailsService implements IUserDetailsService { + private _document: ng.IDocumentService; + private _userDetails: Models.IUserDetails; + + constructor($document: ng.IDocumentService) { + this._document = $document; + } + + public getUserDetails(elementId = "userDetails") { + if (!this._userDetails) { + //var el = this._document.querySelector("[data-json-id='" + elementId + "']"); + var el = this._document.find("#" + elementId + "[type='application/json']"); + + if (el.length) { + this._userDetails = angular.fromJson(el.text()); + } else { + this._userDetails = { + isAuthenticated: false, + userId: null, + userName: null, + roles: [] + }; + } + } + return this._userDetails; + } + } + + angular.module("MusicStore.UserDetails") + .service("MusicStore.UserDetails.IUserDetailsService", [ + "$document", + UserDetailsService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts new file mode 100644 index 0000000000..8f0229275b --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts @@ -0,0 +1,34 @@ +module MusicStore.UserDetails { + export interface IUserDetailsService { + getUserDetails(): Models.IUserDetails; + getUserDetails(elementId: string): Models.IUserDetails; + } + + class UserDetailsService implements IUserDetailsService { + private _document: ng.IDocumentService; + private _userDetails: Models.IUserDetails; + + constructor($document: ng.IDocumentService) { + this._document = $document; + } + + public getUserDetails(elementId = "userDetails") { + if (!this._userDetails) { + //var el = this._document.querySelector("[data-json-id='" + elementId + "']"); + var el = this._document.find("#" + elementId + "[type='application/json']"); + + if (el.length) { + this._userDetails = angular.fromJson(el.text()); + } else { + this._userDetails = { + isAuthenticated: false, + userId: null, + userName: null, + roles: [] + }; + } + } + return this._userDetails; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ng.ts new file mode 100644 index 0000000000..71b96921f7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ng.ts @@ -0,0 +1,16 @@ +/// + +module MusicStore.ViewAlert { + export interface IViewAlertService { + alert: Models.IAlert; + } + + class ViewAlertService implements IViewAlertService { + public alert: Models.IAlert; + } + + angular.module("MusicStore.ViewAlert") + .service("MusicStore.ViewAlert.IViewAlertService", [ + ViewAlertService + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts new file mode 100644 index 0000000000..488dc39010 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts @@ -0,0 +1,9 @@ +module MusicStore.ViewAlert { + export interface IViewAlertService { + alert: Models.IAlert; + } + + class ViewAlertService implements IViewAlertService { + public alert: Models.IAlert; + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ng.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ng.ts new file mode 100644 index 0000000000..797af5f8f6 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ng.ts @@ -0,0 +1,70 @@ +/// + +module MusicStore.Visited { + interface IVisitedFormController extends ng.IFormController { + focus?: boolean; + visited?: boolean; + } + + //@NgDirective('input') + //@NgDirective('select') + class VisitedDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor($window: ng.IWindowService) { + for (var m in this) { + if (this[m].bind) { + this[m] = this[m].bind(this); + } + } + this._window = $window; + } + + public restrict = "E"; + + public require = "?ngModel"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: IVisitedFormController) { + if (!ctrl) { + return; + } + + element.on("focus", event => { + element.addClass("has-focus"); + scope.$apply(() => ctrl.focus = true); + }); + + element.on("blur", event => { + element.removeClass("has-focus"); + element.addClass("has-visited"); + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + + element.closest("form").on("submit", function () { + element.addClass("has-visited"); + + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + } + } + + angular.module("MusicStore.Visited") + .directive("input", [ + "$window", + function (a) { + return new VisitedDirective(a); + } + ]) + .directive("select", [ + "$window", + function (a) { + return new VisitedDirective(a); + } + ]); +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts b/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts new file mode 100644 index 0000000000..8459648052 --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts @@ -0,0 +1,49 @@ +module MusicStore.Visited { + interface IVisitedFormController extends ng.IFormController { + focus?: boolean; + visited?: boolean; + } + + //@NgDirective('input') + //@NgDirective('select') + class VisitedDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor($window: ng.IWindowService) { + this._window = $window; + } + + public restrict = "E"; + + public require = "?ngModel"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: IVisitedFormController) { + if (!ctrl) { + return; + } + + element.on("focus", event => { + element.addClass("has-focus"); + scope.$apply(() => ctrl.focus = true); + }); + + element.on("blur", event => { + element.removeClass("has-focus"); + element.addClass("has-visited"); + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + + element.closest("form").on("submit", function () { + element.addClass("has-visited"); + + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Client/ng-apps/references.ts b/src/MvcMusicStore.Spa/Client/ng-apps/references.ts new file mode 100644 index 0000000000..102e291cec --- /dev/null +++ b/src/MvcMusicStore.Spa/Client/ng-apps/references.ts @@ -0,0 +1,9 @@ +/// +/// +/// + +declare module ng { + export interface ILogProvider { + debugEnabled(enabled: boolean); + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Controllers/AccountController.cs b/src/MvcMusicStore.Spa/Controllers/AccountController.cs new file mode 100644 index 0000000000..e63b2b3b42 --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/AccountController.cs @@ -0,0 +1,421 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Controllers +{ + [Authorize] + public class AccountController : Controller + { + public enum ManageMessageId + { + ChangePasswordSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + Error + } + + private const string XsrfKey = "XsrfId"; + + private UserManager _userManager; + + public AccountController() + : this(new UserManager(new UserStore(new ApplicationDbContext()))) + { + } + + public AccountController(UserManager userManager) + { + _userManager = userManager; + } + + private IAuthenticationManager AuthenticationManager + { + get { return HttpContext.GetOwinContext().Authentication; } + } + + private async Task MigrateShoppingCart(string userName) + { + using (var storeContext = new MusicStoreEntities()) + { + var cart = ShoppingCart.GetCart(storeContext, this); + + await cart.MigrateCart(userName); + + Session[ShoppingCart.CartSessionKey] = userName; + } + } + + // GET: /Account/Login + [AllowAnonymous] + public ActionResult Login(string returnUrl) + { + ViewBag.ReturnUrl = returnUrl; + + return View(); + } + + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl) + { + if (ModelState.IsValid) + { + var user = await _userManager.FindAsync(model.UserName, model.Password); + if (user != null) + { + await SignInAsync(user, model.RememberMe); + + return RedirectToLocal(returnUrl); + } + + ModelState.AddModelError("", "Invalid username or password."); + } + + return View(model); + } + + // GET: /Account/Register + [AllowAnonymous] + public ActionResult Register() + { + return View(); + } + + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model) + { + if (ModelState.IsValid) + { + var user = new ApplicationUser { UserName = model.UserName }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + await SignInAsync(user, isPersistent: false); + + return RedirectToAction("Index", "Home"); + } + + AddErrors(result); + } + + return View(model); + } + + // POST: /Account/Disassociate + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disassociate(string loginProvider, string providerKey) + { + var result = await _userManager.RemoveLoginAsync( + User.Identity.GetUserId(), + new UserLoginInfo(loginProvider, providerKey)); + + return RedirectToAction( + "Manage", + new { Message = result.Succeeded ? ManageMessageId.RemoveLoginSuccess : ManageMessageId.Error }); + } + + // GET: /Account/Manage + public async Task Manage(ManageMessageId? message) + { + switch (message) + { + case ManageMessageId.ChangePasswordSuccess: + ViewBag.StatusMessage = "Your password has been changed."; + break; + case ManageMessageId.SetPasswordSuccess: + ViewBag.StatusMessage = "Your password has been set."; + break; + case ManageMessageId.RemoveLoginSuccess: + ViewBag.StatusMessage = "The external login was removed."; + break; + case ManageMessageId.Error: + ViewBag.StatusMessage = "An error has occurred."; + break; + default: + ViewBag.StatusMessage = ""; + break; + } + + ViewBag.HasLocalPassword = await HasPasswordAsync(); + ViewBag.ReturnUrl = Url.Action("Manage"); + + return View(); + } + + // POST: /Account/Manage + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Manage(ManageUserViewModel model) + { + bool hasPassword = await HasPasswordAsync(); + ViewBag.HasLocalPassword = hasPassword; + ViewBag.ReturnUrl = Url.Action("Manage"); + + if (hasPassword) + { + if (ModelState.IsValid) + { + var result = await _userManager.ChangePasswordAsync( + User.Identity.GetUserId(), + model.OldPassword, + model.NewPassword); + + if (result.Succeeded) + { + return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); + } + + AddErrors(result); + } + } + else + { + var state = ModelState["OldPassword"]; + if (state != null) + { + state.Errors.Clear(); + } + + if (ModelState.IsValid) + { + var result = await _userManager.AddPasswordAsync( + User.Identity.GetUserId(), + model.NewPassword); + + if (result.Succeeded) + { + return RedirectToAction("Manage", new { Message = ManageMessageId.SetPasswordSuccess }); + } + + AddErrors(result); + } + } + + return View(model); + } + + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult ExternalLogin(string provider, string returnUrl) + { + return new ChallengeResult(provider, + Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl })); + } + + // GET: /Account/ExternalLoginCallback + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync(); + if (loginInfo == null) + { + return RedirectToAction("Login"); + } + + var user = await _userManager.FindAsync(loginInfo.Login); + if (user != null) + { + await SignInAsync(user, false); + + return RedirectToLocal(returnUrl); + } + + ViewBag.ReturnUrl = returnUrl; + ViewBag.LoginProvider = loginInfo.Login.LoginProvider; + + return View("ExternalLoginConfirmation", + new ExternalLoginConfirmationViewModel { UserName = loginInfo.DefaultUserName }); + } + + // POST: /Account/LinkLogin + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult LinkLogin(string provider) + { + return new ChallengeResult(provider, Url.Action("LinkLoginCallback", "Account"), User.Identity.GetUserId()); + } + + // GET: /Account/LinkLoginCallback + public async Task LinkLoginCallback() + { + var loginInfo = + await AuthenticationManager.GetExternalLoginInfoAsync(XsrfKey, User.Identity.GetUserId()); + + if (loginInfo == null) + { + return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); + } + + var result = await _userManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); + if (result.Succeeded) + { + return RedirectToAction("Manage"); + } + + return RedirectToAction("Manage", new { Message = ManageMessageId.Error }); + } + + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task ExternalLoginConfirmation( + ExternalLoginConfirmationViewModel model, + string returnUrl) + { + if (User.Identity.IsAuthenticated) + { + return RedirectToAction("Manage"); + } + + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await AuthenticationManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return View("ExternalLoginFailure"); + } + + var user = new ApplicationUser { UserName = model.UserName }; + + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user.Id, info.Login); + + if (result.Succeeded) + { + await SignInAsync(user, false); + return RedirectToLocal(returnUrl); + } + } + + AddErrors(result); + } + + ViewBag.ReturnUrl = returnUrl; + + return View(model); + } + + // POST: /Account/LogOff + [HttpPost] + [ValidateAntiForgeryToken] + public ActionResult LogOff() + { + AuthenticationManager.SignOut(); + + return RedirectToAction("Index", "Home"); + } + + // GET: /Account/ExternalLoginFailure + [AllowAnonymous] + public ActionResult ExternalLoginFailure() + { + return View(); + } + + [ChildActionOnly] + public ActionResult RemoveAccountList() + { + var linkedAccounts = _userManager.GetLogins(User.Identity.GetUserId()); + + ViewBag.ShowRemoveButton = HasPassword() || linkedAccounts.Count > 1; + + return PartialView("_RemoveAccountPartial", linkedAccounts); + } + + protected override void Dispose(bool disposing) + { + if (disposing && _userManager != null) + { + _userManager.Dispose(); + _userManager = null; + } + + base.Dispose(disposing); + } + + private async Task SignInAsync(ApplicationUser user, bool isPersistent) + { + AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie); + + var identity = + await _userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); + + AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, identity); + + await MigrateShoppingCart(user.UserName); + } + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error); + } + } + + private bool HasPassword() + { + var user = _userManager.FindById(User.Identity.GetUserId()); + + return user != null && user.PasswordHash != null; + } + + private async Task HasPasswordAsync() + { + var user = await _userManager.FindByIdAsync(User.Identity.GetUserId()); + + return user != null && user.PasswordHash != null; + } + + private ActionResult RedirectToLocal(string returnUrl) + { + return Url.IsLocalUrl(returnUrl) + ? (ActionResult)Redirect(returnUrl) + : RedirectToAction("Index", "Home"); + } + + private class ChallengeResult : HttpUnauthorizedResult + { + private readonly string _loginProvider; + private readonly string _redirectUri; + private readonly string _userId; + + public ChallengeResult(string provider, string redirectUri, string userId = null) + { + _loginProvider = provider; + _redirectUri = redirectUri; + _userId = userId; + } + + public override void ExecuteResult(ControllerContext context) + { + var properties = new AuthenticationProperties { RedirectUri = _redirectUri }; + if (_userId != null) + { + properties.Dictionary[XsrfKey] = _userId; + } + context.HttpContext.GetOwinContext().Authentication.Challenge(properties, _loginProvider); + } + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Controllers/CheckoutController.cs b/src/MvcMusicStore.Spa/Controllers/CheckoutController.cs new file mode 100644 index 0000000000..98b832b767 --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/CheckoutController.cs @@ -0,0 +1,64 @@ +using System; +using System.Data.Entity; +using System.Threading.Tasks; +using System.Web.Mvc; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Controllers +{ + [Authorize] + public class CheckoutController : Controller + { + private const string PromoCode = "FREE"; + + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + // GET: /Checkout/ + public ActionResult AddressAndPayment() + { + return View(); + } + + // POST: /Checkout/AddressAndPayment + [HttpPost] + public async Task AddressAndPayment(FormCollection values) + { + var order = new Order(); + TryUpdateModel(order); + + if (ModelState.IsValid + && string.Equals(values["PromoCode"], PromoCode, StringComparison.OrdinalIgnoreCase)) + { + order.Username = User.Identity.Name; + order.OrderDate = DateTime.Now; + + _storeContext.Orders.Add(order); + + await ShoppingCart.GetCart(_storeContext, this).CreateOrder(order); + + await _storeContext.SaveChangesAsync(); + + return RedirectToAction("Complete", new { id = order.OrderId }); + } + + return View(order); + } + + // GET: /Checkout/Complete + public async Task Complete(int id) + { + return await _storeContext.Orders.AnyAsync(o => o.OrderId == id && o.Username == User.Identity.Name) + ? View(id) + : View("Error"); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Controllers/HomeController.cs b/src/MvcMusicStore.Spa/Controllers/HomeController.cs new file mode 100644 index 0000000000..470f1dd0ab --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/HomeController.cs @@ -0,0 +1,28 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Mvc; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Controllers +{ + public class HomeController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + // GET: /Home/ + public ActionResult Index() + { + return View(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Controllers/ShoppingCartController.cs b/src/MvcMusicStore.Spa/Controllers/ShoppingCartController.cs new file mode 100644 index 0000000000..c8bd97adcf --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/ShoppingCartController.cs @@ -0,0 +1,94 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Mvc; +using MvcMusicStore.Models; +using MvcMusicStore.ViewModels; + +namespace MvcMusicStore.Controllers +{ + public class ShoppingCartController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + // GET: /ShoppingCart/ + public async Task Index() + { + var cart = ShoppingCart.GetCart(_storeContext, this); + + var viewModel = new ShoppingCartViewModel + { + CartItems = await cart.GetCartItems().ToListAsync(), + CartTotal = await cart.GetTotal() + }; + + return View(viewModel); + } + + // GET: /ShoppingCart/AddToCart/5 + public async Task AddToCart(int id) + { + var cart = ShoppingCart.GetCart(_storeContext, this); + + await cart.AddToCart(await _storeContext.Albums.SingleAsync(a => a.AlbumId == id)); + + await _storeContext.SaveChangesAsync(); + + return RedirectToAction("Index"); + } + + // AJAX: /ShoppingCart/RemoveFromCart/5 + [HttpPost] + public async Task RemoveFromCart(int id) + { + var cart = ShoppingCart.GetCart(_storeContext, this); + + var albumName = await _storeContext.Carts + .Where(i => i.RecordId == id) + .Select(i => i.Album.Title) + .SingleOrDefaultAsync(); + + var itemCount = await cart.RemoveFromCart(id); + + await _storeContext.SaveChangesAsync(); + + var removed = (itemCount > 0) ? " 1 copy of " : string.Empty; + + var results = new ShoppingCartRemoveViewModel + { + Message = removed + albumName + " has been removed from your shopping cart.", + CartTotal = await cart.GetTotal(), + CartCount = await cart.GetCount(), + ItemCount = itemCount, + DeleteId = id + }; + + return Json(results); + } + + [ChildActionOnly] + public ActionResult CartSummary() + { + var cart = ShoppingCart.GetCart(_storeContext, this); + + var cartItems = cart.GetCartItems() + .Select(a => a.Album.Title) + .OrderBy(x => x) + .ToList(); + + ViewBag.CartCount = cartItems.Count(); + ViewBag.CartSummary = string.Join("\n", cartItems.Distinct()); + + return PartialView("CartSummary"); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} diff --git a/src/MvcMusicStore.Spa/Controllers/StoreManagerController.cs b/src/MvcMusicStore.Spa/Controllers/StoreManagerController.cs new file mode 100644 index 0000000000..8b78c462a0 --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/StoreManagerController.cs @@ -0,0 +1,124 @@ +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Mvc; +using MvcMusicStore.Models; + +namespace MvcMusicStore.Controllers +{ + [Authorize(Roles = "Administrator")] + public class StoreManagerController : Controller + { + private readonly MusicStoreEntities _storeContext = new MusicStoreEntities(); + + public ActionResult Index() + { + return View(); + } + + // GET: /StoreManager/Create + public async Task Create() + { + return await BuildView(null); + } + + // POST: /StoreManager/Create + [HttpPost] + public async Task Create(Album album) + { + if (ModelState.IsValid) + { + _storeContext.Albums.Add(album); + + await _storeContext.SaveChangesAsync(); + + return RedirectToAction("Index"); + } + + return await BuildView(album); + } + + // GET: /StoreManager/Edit/5 + public async Task Edit(int id = 0) + { + var album = await _storeContext.Albums.FindAsync(id); + if (album == null) + { + return HttpNotFound(); + } + + return await BuildView(album); + } + + // POST: /StoreManager/Edit/5 + [HttpPost] + public async Task Edit(Album album) + { + if (ModelState.IsValid) + { + _storeContext.Entry(album).State = EntityState.Modified; + + await _storeContext.SaveChangesAsync(); + + return RedirectToAction("Index"); + } + + return await BuildView(album); + } + + // GET: /StoreManager/Delete/5 + public async Task Delete(int id = 0) + { + var album = await _storeContext.Albums.FindAsync(id); + if (album == null) + { + return HttpNotFound(); + } + + return View(album); + } + + // POST: /StoreManager/Delete/5 + [HttpPost, ActionName("Delete")] + public async Task DeleteConfirmed(int id) + { + var album = await _storeContext.Albums.FindAsync(id); + if (album == null) + { + return HttpNotFound(); + } + + _storeContext.Albums.Remove(album); + + await _storeContext.SaveChangesAsync(); + + return RedirectToAction("Index"); + } + + private async Task BuildView(Album album) + { + ViewBag.GenreId = new SelectList( + await _storeContext.Genres.ToListAsync(), + "GenreId", + "Name", + album == null ? null : (object)album.GenreId); + + ViewBag.ArtistId = new SelectList( + await _storeContext.Artists.ToListAsync(), + "ArtistId", + "Name", + album == null ? null : (object)album.ArtistId); + + return View(album); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _storeContext.Dispose(); + } + base.Dispose(disposing); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Controllers/TemplateController.cs b/src/MvcMusicStore.Spa/Controllers/TemplateController.cs new file mode 100644 index 0000000000..608e7bf78c --- /dev/null +++ b/src/MvcMusicStore.Spa/Controllers/TemplateController.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; + +namespace MvcMusicStore.Controllers +{ + public class TemplateController : Controller + { + private static readonly string _templateBasePath = "~/Client/ng-apps/"; + + // GET: Template + [Route("ng-apps/{*path}")] + public ActionResult Index(string path) + { + if (!IsValidPath(path)) + { + return HttpNotFound(); + } + + return View(_templateBasePath + path); + } + + private static bool IsValidPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + var last = '\0'; + for (var i = 0; i < path.Length; i++) + { + var c = path[i]; + if (Char.IsLetterOrDigit(c) + || (c == '/' && last != '/') + || c == '-' + || c == '_' + || (c == '.' && last != '.')) + { + last = c; + continue; + } + return false; + } + + return path.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Global.asax b/src/MvcMusicStore.Spa/Global.asax new file mode 100644 index 0000000000..8223631056 --- /dev/null +++ b/src/MvcMusicStore.Spa/Global.asax @@ -0,0 +1 @@ +<%@ Application Codebehind="Global.asax.cs" Inherits="MvcMusicStore.MvcApplication" Language="C#" %> diff --git a/src/MvcMusicStore.Spa/Global.asax.cs b/src/MvcMusicStore.Spa/Global.asax.cs new file mode 100644 index 0000000000..57b629f843 --- /dev/null +++ b/src/MvcMusicStore.Spa/Global.asax.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; + +namespace MvcMusicStore +{ + public class MvcApplication : System.Web.HttpApplication + { + protected void Application_Start() + { + AreaRegistration.RegisterAllAreas(); + FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); + RouteConfig.RegisterRoutes(RouteTable.Routes); + } + } +} diff --git a/src/MvcMusicStore.Spa/Gruntfile.js b/src/MvcMusicStore.Spa/Gruntfile.js new file mode 100644 index 0000000000..730c732945 --- /dev/null +++ b/src/MvcMusicStore.Spa/Gruntfile.js @@ -0,0 +1,189 @@ +/// + +// node-debug (Resolve-Path ~\AppData\Roaming\npm\node_modules\grunt-cli\bin\grunt) task:target + +module.exports = function (grunt) { + /// + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-typescript'); + grunt.loadNpmTasks('grunt-tslint'); + grunt.loadNpmTasks('grunt-tsng'); + //grunt.loadNpmTasks('grunt-contrib-jshint'); + //grunt.loadNpmTasks('grunt-contrib-qunit'); + //grunt.loadNpmTasks('grunt-contrib-concat'); + + grunt.initConfig({ + staticFilePattern: '**/*.{js,css,map,html,htm,ico,jpg,jpeg,png,gif,eot,svg,ttf,woff}', + pkg: grunt.file.readJSON('package.json'), + uglify: { + options: { + banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' + }, + release: { + files: { + 'public/app.min.js': ['<%= typescript.dev.dest %>'] + } + } + }, + clean: { + options: { force: true }, + bower: ['public'], + assets: ['public'], + tsng: ['client/**/*.ng.ts'] + }, + copy: { + // This is to work around an issue with the dt-angular bower package https://github.com/dt-bower/dt-angular/issues/4 + fix: { + files: { + "bower_components/jquery/jquery.d.ts": ["bower_components/dt-jquery/jquery.d.ts"] + } + }, + bower: { + files: [ + { // JavaScript + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "modernizr/modernizr.js", + "jquery/dist/*.{js,map}", + "jquery.validation/jquery.validate.js", + "jquery.validation/additional-methods.js", + "bootstrap/dist/**/*.js", + "respond/dest/**/*.js", + "angular/*.{js,.js.map}", + "angular-route/*.{js,.js.map}", + "angular-bootstrap/ui-bootstrap*" + ], + dest: "public/js/", + options: { force: true } + }, + { // CSS + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "bootstrap/dist/**/*.css", + ], + dest: "public/css/", + options: { force: true } + }, + { // Fonts + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "bootstrap/**/*.{woff,svg,eot,ttf}", + ], + dest: "public/fonts/", + options: { force: true } + } + ] + }, + assets: { + files: [ + { + expand: true, + cwd: "Client/", + src: [ + '<%= staticFilePattern %>' + ], + dest: "public/", + options: { force: true } + } + ] + } + }, + less: { + dev: { + options: { + cleancss: false + }, + files: { + "public/css/site.css": "Client/**/*.less" + } + }, + release: { + options: { + cleancss: true + }, + files: { + "public/css/site.css": "Client/**/*.less" + } + } + }, + tsng: { + options: { + extension: ".ng.ts" + }, + dev: { + files: [ + // TODO: Automate the generation of this config based on convention + { + src: ['Client/ng-apps/components/**/*.ts', 'Client/ng-apps/MusicStore.Store/**/*.ts', "!**/*.ng.ts"], + dest: "Client/ng-apps" // This needs to be the same across all sets so shared components work + }, + { + src: ['Client/ng-apps/components/**/*.ts', 'Client/ng-apps/MusicStore.Admin/**/*.ts', "!**/*.ng.ts"], + dest: "Client/ng-apps" // This needs to be the same across all sets so shared components work + } + ] + } + }, + tslint: { + options: { + configuration: grunt.file.readJSON("tslint.json") + }, + files: { + src: ['Client/**/*.ts', '!**/*.ng.ts'] + } + }, + typescript: { + options: { + module: 'amd', // or commonjs + target: 'es5', // or es3 + sourcemap: false + }, + dev: { + files: [ + // TODO: Automate the generation of this config based on convention + { + src: ['Client/ng-apps/components/**/*.ng.ts', 'Client/ng-apps/MusicStore.Store/**/*.ng.ts'], + dest: 'public/js/MusicStore.Store.js' + }, + { + src: ['Client/ng-apps/components/**/*.ng.ts', 'Client/ng-apps/MusicStore.Admin/**/*.ng.ts'], + dest: 'public/js/MusicStore.Admin.js' + } + ] + }, + release: { + options: { + sourcemap: true + }, + files: '<%= typescript.dev.files %>' + } + }, + watch: { + typescript: { + files: ['Client/**/*.ts', "!**/*.ng.ts"], + tasks: ['ts'] + }, + dev: { + files: ['bower_components/<%= staticFilePattern %>', 'Client/<%= staticFilePattern %>'], + tasks: ['dev'] + } + } + }); + + //grunt.registerTask('test', ['jshint', 'qunit']); + grunt.registerTask('ts', ['tslint', 'tsng', 'typescript:dev']); + grunt.registerTask('dev', ['clean', 'copy', 'less:dev', 'ts']); + grunt.registerTask('release', ['clean', 'copy', 'uglify', 'less:release', 'typescript:release']); + grunt.registerTask('default', ['dev']); +}; \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Helpers/AngularExtensions.cs b/src/MvcMusicStore.Spa/Helpers/AngularExtensions.cs new file mode 100644 index 0000000000..3d0b40ec05 --- /dev/null +++ b/src/MvcMusicStore.Spa/Helpers/AngularExtensions.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Linq.Expressions; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; + +namespace System.Web.Mvc.Html +{ + public static class AngularExtensions + { + public static IHtmlString ngPasswordFor(this HtmlHelper html, Expression> expression) + { + return html.ngTextBoxFor(expression, new RouteValueDictionary { { "type", "password" } }); + } + + public static IHtmlString ngPasswordFor(this HtmlHelper html, Expression> expression, object htmlAttributes) + { + return html.ngTextBoxFor(expression, MergeAttributes( + new RouteValueDictionary { { "type", "password" } }, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); + } + + public static IHtmlString ngPasswordFor(this HtmlHelper html, Expression> expression, IDictionary htmlAttributes) + { + return html.ngTextBoxFor(expression, MergeAttributes( + new RouteValueDictionary { { "type", "password" } }, + htmlAttributes)); + } + + public static IHtmlString ngTextBoxFor(this HtmlHelper html, Expression> expression) + { + return html.ngTextBoxFor(expression, new RouteValueDictionary()); + } + + public static IHtmlString ngTextBoxFor(this HtmlHelper html, Expression> expression, object htmlAttributes) + { + return html.ngTextBoxFor(expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static IHtmlString ngTextBoxFor(this HtmlHelper html, Expression> expression, IDictionary htmlAttributes) + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + var ngAttributes = new Dictionary(); + + // Angular binding to client-side model (scope). This is required for Angular validation to work. + ngAttributes["ng-model"] = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText); + + // Set input type + if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.EmailAddress), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "email"; + } + else if (metadata.ModelType == typeof(Uri) + || string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Url), StringComparison.OrdinalIgnoreCase) + || string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.ImageUrl), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "url"; + } + else if (IsNumberType(metadata.ModelType)) + { + ngAttributes["type"] = "number"; + if (IsIntegerType(metadata.ModelType)) + { + ngAttributes["step"] = "1"; + } + else + { + ngAttributes["step"] = "any"; + } + } + else if (metadata.ModelType == typeof(DateTime)) + { + if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Date), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "date"; + } + else if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.DateTime), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "datetime"; + } + } + + // Add attributes for Angular validation + var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext) + .SelectMany(v => v.GetClientValidationRules()); + + foreach (var validator in clientValidators) + { + if (string.Equals(validator.ValidationType, "length")) + { + if (validator.ValidationParameters.ContainsKey("min")) + { + ngAttributes["ng-minlength"] = validator.ValidationParameters["min"]; + } + if (validator.ValidationParameters.ContainsKey("max")) + { + ngAttributes["ng-maxlength"] = validator.ValidationParameters["max"]; + } + } + else if (string.Equals(validator.ValidationType, "required")) + { + ngAttributes["required"] = null; + } + else if (string.Equals(validator.ValidationType, "range")) + { + if (validator.ValidationParameters.ContainsKey("min")) + { + ngAttributes["min"] = validator.ValidationParameters["min"]; + } + if (validator.ValidationParameters.ContainsKey("max")) + { + ngAttributes["max"] = validator.ValidationParameters["max"]; + } + } + else if (string.Equals(validator.ValidationType, "equalto")) + { + // CompareAttribute validator + var fieldToCompare = validator.ValidationParameters["other"]; // e.g. *.NewPassword + var other = validator.ValidationParameters["other"].ToString(); + if (other.StartsWith("*.")) + { + // The built-in CompareAttributeAdapter prepends *. to the property name so we strip it off here + other = other.Substring("*.".Length); + } + ngAttributes["app-equal-to"] = other; + // TODO: Actually write the Angular directive to use this + } + // TODO: Regex, Phone(regex) + } + + // Render! + return html.TextBoxFor(expression, MergeAttributes(ngAttributes, htmlAttributes)); + } + + private static bool IsNumberType(Type type) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + return true; + } + return false; + } + + private static bool IsIntegerType(Type type) + { + switch (Type.GetTypeCode(type)) + { + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return true; + } + return false; + } + + public static IHtmlString ngDropDownListFor(this HtmlHelper html, Expression> propertyExpression, Expression> displayExpression, string source, string nullOption, object htmlAttributes) + { + return ngDropDownListFor(html, propertyExpression, displayExpression, source, nullOption, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static IHtmlString ngDropDownListFor(this HtmlHelper html, Expression> propertyExpression, Expression> displayExpression, string source, string nullOption, IDictionary htmlAttributes) + { + var propertyExpressionText = ExpressionHelper.GetExpressionText(propertyExpression); + var displayExpressionText = ExpressionHelper.GetExpressionText(displayExpression); + var metadata = ModelMetadata.FromLambdaExpression(propertyExpression, html.ViewData); + var tag = new TagBuilder("select"); + + var valueFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyExpressionText); + var displayFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(displayExpressionText); + + var displayFieldNameParts = displayFieldName.Split('.'); + displayFieldName = displayFieldNameParts[displayFieldNameParts.Length - 1]; + + tag.Attributes["id"] = html.ViewData.TemplateInfo.GetFullHtmlFieldId(propertyExpressionText); + tag.Attributes["name"] = valueFieldName; + tag.Attributes["ng-model"] = valueFieldName; + + var ngOptionsFormat = "a.{0} as a.{1} for a in {2}"; + var ngOptions = string.Format(ngOptionsFormat, valueFieldName, displayFieldName, source); + tag.Attributes["ng-options"] = ngOptions; + + if (nullOption != null) + { + var nullOptionTag = new TagBuilder("option"); + nullOptionTag.Attributes["value"] = string.Empty; + nullOptionTag.SetInnerText(nullOption); + tag.InnerHtml = nullOptionTag.ToString(); + } + + if (metadata.IsRequired) + { + tag.Attributes["required"] = string.Empty; + } + + tag.MergeAttributes(htmlAttributes, replaceExisting: true); + + return html.Raw(tag.ToString()); + } + + public static IHtmlString ngValidationMessageFor(this HtmlHelper htmlHelper, Expression> expression, string formName) + { + return ngValidationMessageFor(htmlHelper, expression, formName, ((IDictionary)new RouteValueDictionary())); + } + + public static IHtmlString ngValidationMessageFor(this HtmlHelper htmlHelper, Expression> expression, string formName, object htmlAttributes) + { + return ngValidationMessageFor(htmlHelper, expression, formName, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static IHtmlString ngValidationMessageFor(this HtmlHelper html, Expression> expression, string formName, IDictionary htmlAttributes) + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText); + + var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext) + .SelectMany(v => v.GetClientValidationRules()); + var tags = new List(); + + // Get validation messages from data type + // TODO: How to get validation messages from model metadata? All methods/properties required seem protected internal :( + + foreach (var validator in clientValidators) + { + var tag = new TagBuilder("span"); + tag.Attributes["ng-cloak"] = string.Empty; + + if (string.Equals(validator.ValidationType, "required")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && {0}.{1}.$error.{2}", formName, modelName, "required"); + tag.SetInnerText(validator.ErrorMessage); + } + else if (string.Equals(validator.ValidationType, "length")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.minlength || {0}.{1}.$error.maxlength)", + formName, modelName); + tag.SetInnerText(validator.ErrorMessage); + } + else if (string.Equals(validator.ValidationType, "range")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.min || {0}.{1}.$error.max)", + formName, modelName); + tag.SetInnerText(validator.ErrorMessage); + } + // TODO: Regex, equalto, remote + else + { + continue; + } + + tag.MergeAttributes(htmlAttributes); + tags.Add(tag); + } + + return html.Raw(String.Concat(tags.Select(t => t.ToString()))); + } + + public static string ngValidationClassFor(this HtmlHelper html, Expression> expression, string formName, string className) + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData); + var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText); + var ngClassFormat = "{{ '{0}' : ({1}.submitAttempted || {1}.{2}.$dirty || {1}.{2}.visited) && {1}.{2}.$invalid }}"; + + return string.Format(ngClassFormat, className, formName, modelName); + } + + private static IDictionary MergeAttributes(IDictionary source, IDictionary target) + { + // Keys in target win over keys in source + foreach (var pair in source) + { + if (!target.ContainsKey(pair.Key)) + { + target[pair.Key] = pair.Value; + } + } + + return target; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Helpers/GeneralExtensions.cs b/src/MvcMusicStore.Spa/Helpers/GeneralExtensions.cs new file mode 100644 index 0000000000..ba15ca5733 --- /dev/null +++ b/src/MvcMusicStore.Spa/Helpers/GeneralExtensions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace System.Web.Mvc.Html +{ + public static class GeneralExtensions + { + public static IHtmlString Tag(this HtmlHelper htmlHelper, TagBuilder tagBuilder) + { + return htmlHelper.Raw(tagBuilder.ToString()); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Helpers/JsonExtensions.cs b/src/MvcMusicStore.Spa/Helpers/JsonExtensions.cs new file mode 100644 index 0000000000..aefa3eda85 --- /dev/null +++ b/src/MvcMusicStore.Spa/Helpers/JsonExtensions.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Web.Routing; +using Newtonsoft.Json; + +namespace System.Web.Mvc.Html +{ + public static class JsonExtensions + { + public static IHtmlString Json(this HtmlHelper helper, TData data) + { + return Json(helper, data, ((IDictionary)new RouteValueDictionary())); + } + + public static IHtmlString Json(this HtmlHelper helper, TData data, object htmlAttributes) + { + return Json(helper, data, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static IHtmlString Json(this HtmlHelper helper, TData data, IDictionary htmlAttributes) + { + var builder = new TagBuilder("script"); + builder.Attributes["type"] = "application/json"; + builder.MergeAttributes(htmlAttributes); + builder.InnerHtml = + (data is JsonString + ? data.ToString() + : JsonConvert.SerializeObject(data)) + .Replace("<", "\u003C").Replace(">", "\u003E"); + + return helper.Tag(builder); + } + + public static IHtmlString InlineData(this HtmlHelper helper, string actionName, string controllerName) + { + var result = helper.Action(actionName, controllerName); + var urlHelper = new UrlHelper(helper.ViewContext.RequestContext); + var url = urlHelper.Action(actionName, controllerName); + + return helper.Json(new JsonString(result), new RouteValueDictionary { + { "app-inline-data", null }, + { "for", url } + }); + } + } + + public class JsonString + { + public JsonString(object value) + : this(value.ToString()) + { + + } + + public JsonString(string value) + { + Value = value; + } + + public string Value { get; private set; } + + public override string ToString() + { + return Value; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Infrastructure/ApiResult.cs b/src/MvcMusicStore.Spa/Infrastructure/ApiResult.cs new file mode 100644 index 0000000000..40863926ee --- /dev/null +++ b/src/MvcMusicStore.Spa/Infrastructure/ApiResult.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Mvc; +using Newtonsoft.Json; + +namespace System.Web.Mvc +{ + public class ApiResult : ActionResult + { + public ApiResult(ModelStateDictionary modelState) + : this() + { + if (modelState.Any(m => m.Value.Errors.Count > 0)) + { + StatusCode = 400; + Message = "The model submitted was invalid. Please correct the specified errors and try again."; + ModelErrors = modelState.SelectMany(m => m.Value.Errors.Select(me => new ModelError { FieldName = m.Key, ErrorMessage = me.ErrorMessage })); + } + } + + public ApiResult() + { + + } + + [JsonIgnore] + public int? StatusCode { get; set; } + + public string Message { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public object Data { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable ModelErrors { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + var json = new SmartJsonResult + { + StatusCode = StatusCode, + Data = this + }; + json.ExecuteResult(context); + } + + public class ModelError + { + public string FieldName { get; set; } + + public string ErrorMessage { get; set; } + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Infrastructure/ForcedModelError.cs b/src/MvcMusicStore.Spa/Infrastructure/ForcedModelError.cs new file mode 100644 index 0000000000..4c74b65d04 --- /dev/null +++ b/src/MvcMusicStore.Spa/Infrastructure/ForcedModelError.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace System.ComponentModel.DataAnnotations +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class ForcedModelErrorAttribute : ValidationAttribute + { + public ForcedModelErrorAttribute(object failValue) + { + FailValue = failValue; + } + + public object FailValue { get; private set; } + + public override string FormatErrorMessage(string name) + { + return string.Format(CultureInfo.CurrentCulture, "The field {0} was forced to fail model validation.", name); + } + + public override bool IsValid(object value) + { +#if DEBUG + return value == null || !value.Equals(FailValue); +#else + return true; +#endif + } + } +} diff --git a/src/MvcMusicStore.Spa/Infrastructure/PagedList.cs b/src/MvcMusicStore.Spa/Infrastructure/PagedList.cs new file mode 100644 index 0000000000..a2c168f9a3 --- /dev/null +++ b/src/MvcMusicStore.Spa/Infrastructure/PagedList.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; + +namespace MvcMusicStore.Infrastructure +{ + public interface IPagedList + { + IEnumerable Data { get; } + + int Page { get; } + + int PageSize { get; } + + int TotalCount { get; } + } + + internal class PagedList : IPagedList + { + public PagedList(IEnumerable data, int page, int pageSize, int totalCount) + { + Data = data; + Page = page; + PageSize = pageSize; + TotalCount = totalCount; + } + + public IEnumerable Data { get; private set; } + + public int Page { get; private set; } + + public int PageSize { get; private set; } + + public int TotalCount{get; private set; } + } + + public static class PagedListExtensions + { + public static IPagedList ToPagedList(this IQueryable query, int page, int pageSize) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + + var data = query.Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToList(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = query.Take(pagingConfig.PageSize) + .ToList(); + } + + return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, query.Count()); + } + + public static async Task> ToPagedListAsync(this IQueryable query, int page, int pageSize) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + + var data = await query.Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToListAsync(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = await query.Take(pagingConfig.PageSize) + .ToListAsync(); + } + + return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, await query.CountAsync()); + } + + private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig) + { + if (pagingConfig.Page < 1) + { + pagingConfig.Page = 1; + } + + if (pagingConfig.PageSize < 10) + { + pagingConfig.PageSize = 10; + } + + if (pagingConfig.PageSize > 100) + { + pagingConfig.PageSize = 100; + } + + return pagingConfig.PageSize * (pagingConfig.Page - 1); + } + + internal class PagingConfig + { + public PagingConfig(int page, int pageSize) + { + Page = page; + PageSize = pageSize; + } + + public int Page { get; set; } + + public int PageSize { get; set; } + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Infrastructure/SmartJsonResult.cs b/src/MvcMusicStore.Spa/Infrastructure/SmartJsonResult.cs new file mode 100644 index 0000000000..f5330fa424 --- /dev/null +++ b/src/MvcMusicStore.Spa/Infrastructure/SmartJsonResult.cs @@ -0,0 +1,35 @@ +using System.Text; +using System.Web.Mvc; +using Newtonsoft.Json; + +namespace System.Web.Mvc +{ + public class SmartJsonResult : ActionResult + { + public SmartJsonResult() : base() + { + + } + + public JsonSerializerSettings Settings { get; set; } + + public object Data { get; set; } + + public int? StatusCode { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + if (!context.IsChildAction) + { + if (StatusCode.HasValue) + { + context.HttpContext.Response.StatusCode = StatusCode.Value; + } + context.HttpContext.Response.ContentType = "application/json"; + context.HttpContext.Response.ContentEncoding = Encoding.UTF8; + } + + context.HttpContext.Response.Write(JsonConvert.SerializeObject(Data, Settings ?? new JsonSerializerSettings())); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Infrastructure/SortExpression.cs b/src/MvcMusicStore.Spa/Infrastructure/SortExpression.cs new file mode 100644 index 0000000000..1d7684990b --- /dev/null +++ b/src/MvcMusicStore.Spa/Infrastructure/SortExpression.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Web; +using System.Web.Helpers; +using System.Web.Mvc; + +namespace MvcMusicStore.Infrastructure +{ + public static class SortExpression + { + private const string SORT_DIRECTION_DESC = " DESC"; + + public static IQueryable SortBy(this IQueryable query, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) where TModel : class + { + return SortBy(query, sortExpression ?? Create(defaultSortExpression, defaultSortDirection)); + } + + public static string Create(Expression> expression, SortDirection sortDirection = SortDirection.Ascending) where TModel : class + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + // TODO: Validate the expression depth, etc. + + var sortExpression = expressionText; + + if (sortDirection == SortDirection.Descending) + { + sortExpression += SORT_DIRECTION_DESC; + } + + return sortExpression; + } + + public static IQueryable SortBy(this IQueryable source, string sortExpression) where T : class + { + + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (String.IsNullOrWhiteSpace(sortExpression)) + { + return source; + } + + sortExpression = sortExpression.Trim(); + bool isDescending = false; + + // DataSource control passes the sort parameter with a direction + // if the direction is descending + if (sortExpression.EndsWith(SORT_DIRECTION_DESC, StringComparison.OrdinalIgnoreCase)) + { + isDescending = true; + int descIndex = sortExpression.Length - SORT_DIRECTION_DESC.Length; + sortExpression = sortExpression.Substring(0, descIndex).Trim(); + } + + if (String.IsNullOrEmpty(sortExpression)) + { + return source; + } + + ParameterExpression parameter = Expression.Parameter(source.ElementType, String.Empty); + + // Build up the property expression, e.g.: (m => m.Foo.Bar) + var sortExpressionParts = sortExpression.Split('.'); + Expression propertyExpression = parameter; + foreach (var property in sortExpressionParts) + { + propertyExpression = Expression.Property(propertyExpression, property); + } + + LambdaExpression lambda = Expression.Lambda(propertyExpression, parameter); + + string methodName = (isDescending) ? "OrderByDescending" : "OrderBy"; + + Expression methodCallExpression = Expression.Call( + typeof(Queryable), methodName, + new Type[] { source.ElementType, propertyExpression.Type }, + source.Expression, + Expression.Quote(lambda)); + + return (IQueryable)source.Provider.CreateQuery(methodCallExpression); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/AccountViewModels.cs b/src/MvcMusicStore.Spa/Models/AccountViewModels.cs new file mode 100644 index 0000000000..994200cd3c --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/AccountViewModels.cs @@ -0,0 +1,65 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace MvcMusicStore.Models +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + } + + public class ManageUserViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class LoginViewModel + { + [Required] + [Display(Name = "User name")] + [StringLength(100, MinimumLength = 3)] + public string UserName { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} diff --git a/src/MvcMusicStore.Spa/Models/Album.cs b/src/MvcMusicStore.Spa/Models/Album.cs new file mode 100644 index 0000000000..b9f88827cc --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/Album.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace MvcMusicStore.Models +{ + public class Album + { + [ScaffoldColumn(false)] + public int AlbumId { get; set; } + + public int GenreId { get; set; } + + public int ArtistId { get; set; } + + [Required] + [StringLength(160, MinimumLength = 2)] + [ForcedModelError("Forced Error: Title")] + public string Title { get; set; } + + [Required] + [Range(0.01, 100.00)] + [DataType(DataType.Currency)] + [ForcedModelError(-1)] + public decimal Price { get; set; } + + [DisplayName("Album Art URL")] + [StringLength(1024)] + [ForcedModelError("Forced Error: AlbumArtUrl")] + public string AlbumArtUrl { get; set; } + + public virtual Genre Genre { get; set; } + + public virtual Artist Artist { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public virtual List OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/Artist.cs b/src/MvcMusicStore.Spa/Models/Artist.cs new file mode 100644 index 0000000000..20a1e49c1d --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/Artist.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace MvcMusicStore.Models +{ + public class Artist + { + public int ArtistId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/Cart.cs b/src/MvcMusicStore.Spa/Models/Cart.cs new file mode 100644 index 0000000000..01507a2eb5 --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/Cart.cs @@ -0,0 +1,19 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace MvcMusicStore.Models +{ + public class Cart + { + [Key] + public int RecordId { get; set; } + public string CartId { get; set; } + public int AlbumId { get; set; } + public int Count { get; set; } + + [DataType(DataType.DateTime)] + public DateTime DateCreated { get; set; } + + public virtual Album Album { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/Genre.cs b/src/MvcMusicStore.Spa/Models/Genre.cs new file mode 100644 index 0000000000..88e67ea4d1 --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/Genre.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace MvcMusicStore.Models +{ + public class Genre + { + public int GenreId { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + [JsonIgnore] + public List Albums { get; set; } + } +} diff --git a/src/MvcMusicStore.Spa/Models/IdentityModels.cs b/src/MvcMusicStore.Spa/Models/IdentityModels.cs new file mode 100644 index 0000000000..464dc16d7e --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/IdentityModels.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNet.Identity.EntityFramework; + +namespace MvcMusicStore.Models +{ + // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. + public class ApplicationUser : IdentityUser + { + } + + public class ApplicationDbContext : IdentityDbContext + { + public ApplicationDbContext() + : base("DefaultConnection") + { + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/MusicStoreEntities.cs b/src/MvcMusicStore.Spa/Models/MusicStoreEntities.cs new file mode 100644 index 0000000000..a62403a0dc --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/MusicStoreEntities.cs @@ -0,0 +1,21 @@ +using System.Data.Entity; + +namespace MvcMusicStore.Models +{ + public class MusicStoreEntities : DbContext + { + public MusicStoreEntities() + : base("name=MusicStoreEntities") + { + Configuration.ProxyCreationEnabled = false; + Configuration.LazyLoadingEnabled = false; + } + + public DbSet Albums { get; set; } + public DbSet Genres { get; set; } + public DbSet Artists { get; set; } + public DbSet Carts { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/Order.cs b/src/MvcMusicStore.Spa/Models/Order.cs new file mode 100644 index 0000000000..0631a6b124 --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/Order.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Web.Mvc; + +namespace MvcMusicStore.Models +{ + [Bind(Include = "FirstName,LastName,Address,City,State,PostalCode,Country,Phone,Email")] + public class Order + { + public Order() + { + OrderDetails = new List(); + } + + [ScaffoldColumn(false)] + public int OrderId { get; set; } + + [ScaffoldColumn(false)] + public System.DateTime OrderDate { get; set; } + + [ScaffoldColumn(false)] + public string Username { get; set; } + + [Required] + [DisplayName("First Name")] + [StringLength(160)] + public string FirstName { get; set; } + + [Required] + [DisplayName("Last Name")] + [StringLength(160)] + public string LastName { get; set; } + + [Required] + [StringLength(70, MinimumLength = 3)] + public string Address { get; set; } + + [Required] + [StringLength(40)] + public string City { get; set; } + + [Required] + [StringLength(40)] + public string State { get; set; } + + [Required] + [DisplayName("Postal Code")] + [StringLength(10, MinimumLength = 5)] + public string PostalCode { get; set; } + + [Required] + [StringLength(40)] + public string Country { get; set; } + + [Required] + [StringLength(24)] + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } + + [Required] + [DisplayName("Email Address")] + [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", + ErrorMessage = "Email is is not valid.")] + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + [ScaffoldColumn(false)] + public decimal Total { get; set; } + + public List OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/OrderDetail.cs b/src/MvcMusicStore.Spa/Models/OrderDetail.cs new file mode 100644 index 0000000000..e52c3efa93 --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/OrderDetail.cs @@ -0,0 +1,14 @@ +namespace MvcMusicStore.Models +{ + public class OrderDetail + { + public int OrderDetailId { get; set; } + public int OrderId { get; set; } + public int AlbumId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + + public virtual Album Album { get; set; } + public virtual Order Order { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/SampleData.cs b/src/MvcMusicStore.Spa/Models/SampleData.cs new file mode 100644 index 0000000000..f143d76d22 --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/SampleData.cs @@ -0,0 +1,4066 @@ +using System.Collections.Generic; +using System.Linq; + +namespace MvcMusicStore.Models +{ + public class SampleData + { + public void Seed(MusicStoreEntities context) + { + const string imgUrl = "/images/placeholder.png"; + + AddAlbums(context, imgUrl, AddGenres(context), AddArtists(context)); + + context.SaveChanges(); + } + + private static void AddAlbums( + MusicStoreEntities context, + string imgUrl, + List genres, + List artists) + { + var albums = new List + { + new Album + { + Title = "The Best Of The Men At Work", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Men At Work"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "...And Justice For All", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = "https://ia601005.us.archive.org/12/items/mbid-fce9462d-8444-334d-84d4-2bbf1edfe9b5/mbid-fce9462d-8444-334d-84d4-2bbf1edfe9b5-5114338029_thumb250.jpg" + }, + new Album + { + Title = "עד גבול האור", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "אריק אינשטיין"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Black Light Syndrome", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Terry Bozzio, Tony Levin & Steve Stevens"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "10,000 Days", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tool"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "11i", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Supreme Beings of Leisure"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "1960", + Genre = genres.Single(g => g.Name == "Indie"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Soul-Junk"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "4x4=12 ", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "deadmau5"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Copland Celebration, Vol. I", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "London Symphony Orchestra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Lively Mind", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paul Oakenfold"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Matter of Life and Death", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Real Dead One", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Real Live One", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Rush of Blood to the Head", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Coldplay"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Soprano Inspired", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Britten Sinfonia, Ivor Bolton & Lesley Garrett"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A Winter Symphony", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Abbey Road", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Beatles"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ace Of Spades", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Motörhead"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Achtung Baby", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Acústico MTV", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Os Paralamas Do Sucesso"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Adams, John: The Chairman Dances", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Edo de Waart & San Francisco Symphony"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Adrenaline", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deftones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ænima", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tool"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Afrociberdelia", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chico Science & Nação Zumbi"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "After the Goldrush", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Neil Young"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Airdrawn Dagger", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sasha"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Album Title Goes Here", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "deadmau5"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Alcohol Fueled Brewtality Live! [Disc 1]", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Black Label Society"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Alcohol Fueled Brewtality Live! [Disc 2]", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Black Label Society"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Alive 2007", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Daft Punk"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "All I Ask of You", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Amen (So Be It)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paddy Casey"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Animal Vehicle", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Axis of Awesome"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ao Vivo [IMPORT]", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Zeca Pagodinho"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Apocalyptic Love", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Slash"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Appetite for Destruction", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Guns N' Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Are You Experienced?", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jimi Hendrix"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Arquivo II", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Os Paralamas Do Sucesso"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Arquivo Os Paralamas Do Sucesso", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Os Paralamas Do Sucesso"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "A-Sides", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Soundgarden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Audioslave", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Audioslave"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Automatic for the People", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "R.E.M."), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Axé Bahia 2001", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Various Artists"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Babel", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Mumford & Sons"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bach: Goldberg Variations", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Wilhelm Kempff"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bach: The Brandenburg Concertos", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Orchestra of The Age of Enlightenment"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bach: The Cello Suites", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Yo-Yo Ma"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bach: Toccata & Fugue in D Minor", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ton Koopman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bad Motorfinger", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Soundgarden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Balls to the Wall", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Accept"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Banadeek Ta'ala", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Amr Diab"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Barbie Girl", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Aqua"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bark at the Moon (Remastered)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bartok: Violin & Viola Concertos", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Yehudi Menuhin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Barulhinho Bom", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Marisa Monte"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "BBC Sessions [Disc 1] [Live]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "BBC Sessions [Disc 2] [Live]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Be Here Now", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Oasis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bedrock 11 Compiled & Mixed", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "John Digweed"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Berlioz: Symphonie Fantastique", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Michael Tilson Thomas"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Beyond Good And Evil", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Cult"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Big Bad Wolf ", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Armand Van Helden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Big Ones", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Aerosmith"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Black Album", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Black Sabbath Vol. 4 (Remaster)", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Black Sabbath"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Black Sabbath", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Black Sabbath"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Black", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Blackwater Park", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Opeth"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Blizzard of Ozz", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Blood", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "In This Moment"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Blue Moods", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Incognito"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Blue", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Weezer"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bongo Fury", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Frank Zappa & Captain Beefheart"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Boys & Girls", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alabama Shakes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Brave New World", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "B-Sides 1980-1990", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Bunkka", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paul Oakenfold"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "By The Way", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Red Hot Chili Peppers"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cake: B-Sides and Rarities", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cake"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Californication", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Red Hot Chili Peppers"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Carmina Burana", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Boston Symphony Orchestra & Seiji Ozawa"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Carried to Dust (Bonus Track Version)", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Calexico"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Carry On", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chris Cornell"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cássia Eller - Sem Limite [Disc 1]", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cássia Eller"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chemical Wedding", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Bruce Dickinson"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chill: Brazil (Disc 1)", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Marcos Valle"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chill: Brazil (Disc 2)", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Antônio Carlos Jobim"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chocolate Starfish And The Hot Dog Flavored Water", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Limp Bizkit"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chronicle, Vol. 1", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Creedence Clearwater Revival"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Chronicle, Vol. 2", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Creedence Clearwater Revival"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ciao, Baby", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "TheStart"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cidade Negra - Hits", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cidade Negra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Classic Munkle: Turbo Edition", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Munkle"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Classics: The Best of Sarah Brightman", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Coda", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Come Away With Me", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Norah Jones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Come Taste The Band", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Comfort Eagle", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cake"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Common Reaction", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Uh Huh Her "), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Compositores", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "O Terço"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Contraband", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Velvet Revolver"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Core", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Stone Temple Pilots"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cornerstone", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Styx"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cosmicolor", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "M-Flo"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Cross", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Justice"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Culture of Fear", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Thievery Corporation"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Da Lama Ao Caos", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chico Science & Nação Zumbi"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Dakshina", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deva Premal"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Dark Side of the Moon", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pink Floyd"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Death Magnetic", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Deep End of Down", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Above the Fold"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Deep Purple In Rock", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Deixa Entrar", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Falamansa"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Deja Vu", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Crosby, Stills, Nash, and Young"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Di Korpu Ku Alma", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Lura"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Diary of a Madman (Remastered)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Diary of a Madman", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Dirt", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alice in Chains"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Diver Down", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Van Halen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Djavan Ao Vivo - Vol. 02", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Djavan"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Djavan Ao Vivo - Vol. 1", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Djavan"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Drum'n'bass for Papa", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Plug"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Duluth", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Trampled By Turtles"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Dummy", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Portishead"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Duos II", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Luciana Souza/Romero Lubambo"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Earl Scruggs and Friends", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Earl Scruggs"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Eden", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "El Camino", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Black Keys"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Elegant Gypsy", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Al di Meola"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Elements Of Life", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tiësto"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Elis Regina-Minha História", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Elis Regina"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Emergency On Planet Earth", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jamiroquai"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Emotion", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Papa Wemba"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "English Renaissance", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The King's Singers"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Every Kind of Light", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Posies"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Faceless", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Godsmack"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Facelift", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alice in Chains"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Fair Warning", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Van Halen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Fear of a Black Planet", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Public Enemy"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Fear Of The Dark", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Feels Like Home", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Norah Jones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Fireball", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Fly", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "For Those About To Rock We Salute You", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "AC/DC"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Four", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Blues Traveler"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Frank", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Amy Winehouse"), + AlbumArtUrl = "http://coverartarchive.org/release/f51a1d11-98aa-4957-9ad1-f1877aee07a8/3487013199-250.jpg" + }, + new Album + { + Title = "Further Down the Spiral", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nine Inch Nails"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Garage Inc. (Disc 1)", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Garage Inc. (Disc 2)", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Garbage", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Garbage"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Good News For People Who Love Bad News", + Genre = genres.Single(g => g.Name == "Indie"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Modest Mouse"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Gordon", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Barenaked Ladies"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Górecki: Symphony No. 3", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Adrian Leaper & Doreen de Feis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Hits I", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Queen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Hits II", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Queen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Hits", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Duck Sauce"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Hits", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Lenny Kravitz"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Hits", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Lenny Kravitz"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greatest Kiss", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Kiss"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Greetings from Michigan", + Genre = genres.Single(g => g.Name == "Indie"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sufjan Stevens"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Group Therapy", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Above & Beyond"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Handel: The Messiah (Highlights)", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Scholars Baroque Ensemble"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Haydn: Symphonies 99 - 104", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Royal Philharmonic Orchestra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Heart of the Night", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Spyro Gyra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Heart On", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Eagles of Death Metal"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Holy Diver", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dio"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Homework", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Daft Punk"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Hot Rocks, 1964-1971 (Disc 1)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Rolling Stones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Houses Of The Holy", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "How To Dismantle An Atomic Bomb", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Human", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Projected"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Hunky Dory", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "David Bowie"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Hymns", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Projected"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Hysteria", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Def Leppard"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Absentia", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Porcupine Tree"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Between", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paul Van Dyk"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Rainbows", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Radiohead"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Step", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Stevie Ray Vaughan & Double Trouble"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In the court of the Crimson King", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "King Crimson"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Through The Out Door", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Your Honor [Disc 1]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Foo Fighters"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "In Your Honor [Disc 2]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Foo Fighters"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Indestructible", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Rancid"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Infinity", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Journey"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Into The Light", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "David Coverdale"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Introspective > You", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pet Shop Boys"), + AlbumArtUrl = "http://coverartarchive.org/release/b3c637f8-ffce-3ed8-a0cf-2b58ecfc1b88/1715773107-250.jpg" + }, + new Album + { + Title = "Iron Maiden", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "ISAM", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Amon Tobin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "IV", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Jagged Little Pill", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alanis Morissette"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Jagged Little Pill", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alanis Morissette"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Jorge Ben Jor 25 Anos", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jorge Ben"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Jota Quest-1995", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jota Quest"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Kick", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "INXS"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Kill 'Em All", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Kind of Blue", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Miles Davis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "King For A Day Fool For A Lifetime", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Faith No More"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Kiss", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Carly Rae Jepsen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Last Call", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cayouche"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Le Freak", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chic"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Le Tigre", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Le Tigre"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Led Zeppelin I", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Led Zeppelin II", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Led Zeppelin III", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Let There Be Rock", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "AC/DC"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Little Earthquakes", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tori Amos"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live [Disc 1]", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Black Crowes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live [Disc 2]", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Black Crowes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live After Death", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live At Donington 1992 (Disc 1)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live At Donington 1992 (Disc 2)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live on Earth", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Cat Empire"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Live On Two Legs [Live]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pearl Jam"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Living After Midnight", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Judas Priest"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Living", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paddy Casey"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Load", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Love Changes Everything", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "MacArthur Park Suite", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Donna Summer"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Machine Head", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Magical Mystery Tour", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Beatles"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mais Do Mesmo", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Legião Urbana"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Maquinarama", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Skank"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Marasim", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jagjit Singh"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mascagni: Cavalleria Rusticana", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "James Levine"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Master of Puppets", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mechanics & Mathematics", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Venus Hum"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mental Jewelry", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Live"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Metallics", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "meteora", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Linkin Park"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Meus Momentos", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Gonzaguinha"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mezmerize", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "System Of A Down"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mezzanine", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Massive Attack"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Miles Ahead", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Miles Davis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Milton Nascimento Ao Vivo", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Milton Nascimento"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Minas", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Milton Nascimento"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Minha Historia", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chico Buarque"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Misplaced Childhood", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Marillion"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "MK III The Final Concerts [Disc 1]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Morning Dance", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Spyro Gyra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Motley Crue Greatest Hits", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Mötley Crüe"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Moving Pictures", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Rush"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mozart: Chamber Music", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nash Ensemble"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Mozart: Symphonies Nos. 40 & 41", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Berliner Philharmoniker"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Murder Ballads", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nick Cave and the Bad Seeds"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Music For The Jilted Generation", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Prodigy"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "My Generation - The Very Best Of The Who", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Who"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "My Name is Skrillex", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Skrillex"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Na Pista", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Cláudio Zoli"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Nevermind", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nirvana"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "New Adventures In Hi-Fi", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "R.E.M."), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "New Divide", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Linkin Park"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "New York Dolls", + Genre = genres.Single(g => g.Name == "Punk"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "New York Dolls"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "News Of The World", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Queen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Nielsen: The Six Symphonies", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Göteborgs Symfoniker & Neeme Järvi"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Night At The Opera", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Queen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Night Castle", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Trans-Siberian Orchestra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Nkolo", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Lokua Kanza"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "No More Tears (Remastered)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "No Prayer For The Dying", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "No Security", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Rolling Stones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "O Brother, Where Art Thou?", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Alison Krauss"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "O Samba Poconé", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Skank"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "O(+>", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Prince"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Oceania", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Smashing Pumpkins"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Off the Deep End", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Weird Al"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "OK Computer", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Radiohead"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Olodum", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Olodum"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "One Love", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "David Guetta"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Operation: Mindcrime", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Queensrÿche"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Opiate", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tool"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Outbreak", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dennis Chambers"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Pachelbel: Canon & Gigue", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "English Concert & Trevor Pinnock"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Paid in Full", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eric B. and Rakim"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Para Siempre", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Vicente Fernandez"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Pause", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Four Tet"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Peace Sells... but Who's Buying", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Megadeth"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Physical Graffiti [Disc 1]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Physical Graffiti [Disc 2]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Physical Graffiti", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Piece Of Mind", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Pinkerton", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Weezer"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Plays Metallica By Four Cellos", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Apocalyptica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Pop", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Powerslave", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Prenda Minha", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Caetano Veloso"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Presence", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Pretty Hate Machine", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nine Inch Nails"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Prisoner", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Jezabels"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Privateering", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Mark Knopfler"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Prokofiev: Romeo & Juliet", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Michael Tilson Thomas"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Prokofiev: Symphony No.1", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sergei Prokofiev & Yuri Temirkanov"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "PSY's Best 6th Part 1", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "PSY"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Purcell: The Fairy Queen", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "London Classical Players"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Purpendicular", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Purple", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Stone Temple Pilots"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Quanta Gente Veio Ver (Live)", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Gilberto Gil"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Quanta Gente Veio ver--Bônus De Carnaval", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Gilberto Gil"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Quiet Songs", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Aisha Duo"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Raices", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Los Tigres del Norte"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Raising Hell", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Run DMC"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Raoul and the Kings of Spain ", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tears For Fears"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rattle And Hum", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Raul Seixas", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Raul Seixas"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Recovery [Explicit]", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eminem"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Reign In Blood", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Slayer"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Relayed", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Yes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "ReLoad", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Respighi:Pines of Rome", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eugene Ormandy"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Restless and Wild", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Accept"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Retrospective I (1974-1980)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Rush"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Revelations", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Audioslave"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Revolver", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Beatles"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ride the Lighting ", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ride The Lightning", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ring My Bell", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Anita Ward"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Riot Act", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pearl Jam"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rise of the Phoenix", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Before the Dawn"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rock In Rio [CD1]", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rock In Rio [CD2]", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rock In Rio [CD2]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Roda De Funk", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Funk Como Le Gusta"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Room for Squares", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "John Mayer"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Root Down", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jimmy Smith"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rounds", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Four Tet"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rubber Factory", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Black Keys"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Rust in Peace", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Megadeth"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Sambas De Enredo 2001", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Various Artists"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Santana - As Years Go By", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Santana"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Santana Live", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Santana"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Saturday Night Fever", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Bee Gees"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Scary Monsters and Nice Sprites", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Skrillex"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Scheherazade", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chicago Symphony Orchestra & Fritz Reiner"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "SCRIABIN: Vers la flamme", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Christopher O'Riley"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Second Coming", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Stone Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Serie Sem Limite (Disc 1)", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tim Maia"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Serie Sem Limite (Disc 2)", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tim Maia"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Serious About Men", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Rubberbandits"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Seventh Son of a Seventh Son", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Short Bus", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Filter"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Sibelius: Finlandia", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Berliner Philharmoniker"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Singles Collection", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "David Bowie"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Six Degrees of Inner Turbulence", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dream Theater"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Slave To The Empire", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "T&N"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Slaves And Masters", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Slouching Towards Bethlehem", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Robert James"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Smash", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Offspring"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Something Special", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dolly Parton"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Somewhere in Time", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Song(s) You Know By Heart", + Genre = genres.Single(g => g.Name == "Country"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Jimmy Buffett"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Sound of Music", + Genre = genres.Single(g => g.Name == "Punk"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Adicts"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "South American Getaway", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The 12 Cellists of The Berlin Philharmonic"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Sozinho Remix Ao Vivo", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Caetano Veloso"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Speak of the Devil", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Spiritual State", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nujabes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "St. Anger", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Metallica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Still Life", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Opeth"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Stop Making Sense", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Talking Heads"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Stormbringer", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Stranger than Fiction", + Genre = genres.Single(g => g.Name == "Punk"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Bad Religion"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Strauss: Waltzes", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eugene Ormandy"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Supermodified", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Amon Tobin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Supernatural", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Santana"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Surfing with the Alien (Remastered)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Joe Satriani"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Switched-On Bach", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Wendy Carlos"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Symphony", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Szymanowski: Piano Works, Vol. 1", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Martin Roscoe"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Tchaikovsky: The Nutcracker", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "London Symphony Orchestra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ted Nugent", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ted Nugent"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Teflon Don", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Rick Ross"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Tell Another Joke at the Ol' Choppin' Block", + Genre = genres.Single(g => g.Name == "Indie"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Danielson Famile"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Temple of the Dog", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Temple of the Dog"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Ten", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pearl Jam"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Texas Flood", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Stevie Ray Vaughan"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Battle Rages On", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Beast Live", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Paul D'Ianno"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best Of 1980-1990", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best of 1990–2000", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best of Beethoven", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Nicolaus Esterhazy Sinfonia"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best Of Billy Cobham", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Billy Cobham"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best of Ed Motta", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ed Motta"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Best Of Van Halen, Vol. I", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Van Halen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Bridge", + Genre = genres.Single(g => g.Name == "R&B"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Melanie Fiona"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Cage", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tygers of Pan Tang"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Chicago Transit Authority", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Chicago "), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Chronic", + Genre = genres.Single(g => g.Name == "Rap"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dr. Dre"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Colour And The Shape", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Foo Fighters"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Crane Wife", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Decemberists"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Cream Of Clapton", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eric Clapton"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Cure", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Cure"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Dark Side Of The Moon", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pink Floyd"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Divine Conspiracy", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Epica"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Doors", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Doors"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Dream of the Blue Turtles", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sting"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Essential Miles Davis [Disc 1]", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Miles Davis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Essential Miles Davis [Disc 2]", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Miles Davis"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Final Concerts (Disc 2)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deep Purple"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Final Frontier", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Head and the Heart", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Head and the Heart"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Joshua Tree", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Last Night of the Proms", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "BBC Concert Orchestra"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Lumineers", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Lumineers"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Number of The Beast", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Number of The Beast", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Police Greatest Hits", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Police"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Song Remains The Same (Disc 1)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Song Remains The Same (Disc 2)", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Southern Harmony and Musical Companion", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Black Crowes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Spade", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Butch Walker & The Black Widows"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Stone Roses", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Stone Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Suburbs", + Genre = genres.Single(g => g.Name == "Indie"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Arcade Fire"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Three Tenors Disc1/Disc2", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Carreras, Pavarotti, Domingo"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Trees They Grow So High", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The Wall", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pink Floyd"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "The X Factor", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Them Crooked Vultures", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Them Crooked Vultures"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "This Is Happening", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "LCD Soundsystem"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Thunder, Lightning, Strike", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Go! Team"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Time to Say Goodbye", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sarah Brightman"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Time, Love & Tenderness", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Michael Bolton"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Tomorrow Starts Today", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Mobile"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Tribute", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Ozzy Osbourne"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Tuesday Night Music Club", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sheryl Crow"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Umoja", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "BLØF"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Under the Pink", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tori Amos"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Undertow", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Tool"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Un-Led-Ed", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Dread Zeppelin"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Unplugged [Live]", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Kiss"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Unplugged", + Genre = genres.Single(g => g.Name == "Blues"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eric Clapton"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Unplugged", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Eric Clapton"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Untrue", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Burial"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Use Your Illusion I", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Guns N' Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Use Your Illusion II", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Guns N' Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Use Your Illusion II", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Guns N' Roses"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Van Halen III", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Van Halen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Van Halen", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Van Halen"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Version 2.0", + Genre = genres.Single(g => g.Name == "Alternative"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Garbage"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Vinicius De Moraes", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Vinícius De Moraes"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Virtual XI", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Iron Maiden"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Voodoo Lounge", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Rolling Stones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Vozes do MPB", + Genre = genres.Single(g => g.Name == "Latin"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Various Artists"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Vs.", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pearl Jam"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Wagner: Favourite Overtures", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sir Georg Solti & Wiener Philharmoniker"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Walking Into Clarksdale", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Page & Plant"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Wapi Yo", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Lokua Kanza"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "War", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Warner 25 Anos", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Antônio Carlos Jobim"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Wasteland R&Btheque", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Raunchy"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Watermark", + Genre = genres.Single(g => g.Name == "Electronic"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Enya"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "We Were Exploding Anyway", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "65daysofstatic"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Weill: The Seven Deadly Sins", + Genre = genres.Single(g => g.Name == "Classical"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Orchestre de l'Opéra de Lyon"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "White Pony", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Deftones"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Who's Next", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Who"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Wish You Were Here", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Pink Floyd"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "With Oden on Our Side", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Amon Amarth"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Worlds", + Genre = genres.Single(g => g.Name == "Jazz"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Aaron Goldberg"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Worship Music", + Genre = genres.Single(g => g.Name == "Metal"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Anthrax"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "X&Y", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Coldplay"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Xinti", + Genre = genres.Single(g => g.Name == "World"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Sara Tavares"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Yano", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Yano"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Yesterday Once More Disc 1/Disc 2", + Genre = genres.Single(g => g.Name == "Pop"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "The Carpenters"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Zooropa", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "U2"), + AlbumArtUrl = imgUrl + }, + new Album + { + Title = "Zoso", + Genre = genres.Single(g => g.Name == "Rock"), + Price = 8.99M, + Artist = artists.Single(a => a.Name == "Led Zeppelin"), + AlbumArtUrl = imgUrl + }, + }; + + context.Albums.AddRange(albums); + } + + private static List AddArtists(MusicStoreEntities context) + { + var artists = new List + { + new Artist { Name = "65daysofstatic" }, + new Artist { Name = "Aaron Goldberg" }, + new Artist { Name = "Above & Beyond" }, + new Artist { Name = "Above the Fold" }, + new Artist { Name = "AC/DC" }, + new Artist { Name = "Accept" }, + new Artist { Name = "Adicts" }, + new Artist { Name = "Adrian Leaper & Doreen de Feis" }, + new Artist { Name = "Aerosmith" }, + new Artist { Name = "Aisha Duo" }, + new Artist { Name = "Al di Meola" }, + new Artist { Name = "Alabama Shakes" }, + new Artist { Name = "Alanis Morissette" }, + new Artist { Name = "Alberto Turco & Nova Schola Gregoriana" }, + new Artist { Name = "Alice in Chains" }, + new Artist { Name = "Alison Krauss" }, + new Artist { Name = "Amon Amarth" }, + new Artist { Name = "Amon Tobin" }, + new Artist { Name = "Amr Diab" }, + new Artist { Name = "Amy Winehouse" }, + new Artist { Name = "Anita Ward" }, + new Artist { Name = "Anthrax" }, + new Artist { Name = "Antônio Carlos Jobim" }, + new Artist { Name = "Apocalyptica" }, + new Artist { Name = "Aqua" }, + new Artist { Name = "Armand Van Helden" }, + new Artist { Name = "Arcade Fire" }, + new Artist { Name = "Audioslave" }, + new Artist { Name = "Bad Religion" }, + new Artist { Name = "Barenaked Ladies" }, + new Artist { Name = "BBC Concert Orchestra" }, + new Artist { Name = "Bee Gees" }, + new Artist { Name = "Before the Dawn" }, + new Artist { Name = "Berliner Philharmoniker" }, + new Artist { Name = "Billy Cobham" }, + new Artist { Name = "Black Label Society" }, + new Artist { Name = "Black Sabbath" }, + new Artist { Name = "BLØF" }, + new Artist { Name = "Blues Traveler" }, + new Artist { Name = "Boston Symphony Orchestra & Seiji Ozawa" }, + new Artist { Name = "Britten Sinfonia, Ivor Bolton & Lesley Garrett" }, + new Artist { Name = "Bruce Dickinson" }, + new Artist { Name = "Buddy Guy" }, + new Artist { Name = "Burial" }, + new Artist { Name = "Butch Walker & The Black Widows" }, + new Artist { Name = "Caetano Veloso" }, + new Artist { Name = "Cake" }, + new Artist { Name = "Calexico" }, + new Artist { Name = "Carly Rae Jepsen" }, + new Artist { Name = "Carreras, Pavarotti, Domingo" }, + new Artist { Name = "Cássia Eller" }, + new Artist { Name = "Cayouche" }, + new Artist { Name = "Chic" }, + new Artist { Name = "Chicago " }, + new Artist { Name = "Chicago Symphony Orchestra & Fritz Reiner" }, + new Artist { Name = "Chico Buarque" }, + new Artist { Name = "Chico Science & Nação Zumbi" }, + new Artist { Name = "Choir Of Westminster Abbey & Simon Preston" }, + new Artist { Name = "Chris Cornell" }, + new Artist { Name = "Christopher O'Riley" }, + new Artist { Name = "Cidade Negra" }, + new Artist { Name = "Cláudio Zoli" }, + new Artist { Name = "Coldplay" }, + new Artist { Name = "Creedence Clearwater Revival" }, + new Artist { Name = "Crosby, Stills, Nash, and Young" }, + new Artist { Name = "Daft Punk" }, + new Artist { Name = "Danielson Famile" }, + new Artist { Name = "David Bowie" }, + new Artist { Name = "David Coverdale" }, + new Artist { Name = "David Guetta" }, + new Artist { Name = "deadmau5" }, + new Artist { Name = "Deep Purple" }, + new Artist { Name = "Def Leppard" }, + new Artist { Name = "Deftones" }, + new Artist { Name = "Dennis Chambers" }, + new Artist { Name = "Deva Premal" }, + new Artist { Name = "Dio" }, + new Artist { Name = "Djavan" }, + new Artist { Name = "Dolly Parton" }, + new Artist { Name = "Donna Summer" }, + new Artist { Name = "Dr. Dre" }, + new Artist { Name = "Dread Zeppelin" }, + new Artist { Name = "Dream Theater" }, + new Artist { Name = "Duck Sauce" }, + new Artist { Name = "Earl Scruggs" }, + new Artist { Name = "Ed Motta" }, + new Artist { Name = "Edo de Waart & San Francisco Symphony" }, + new Artist { Name = "Elis Regina" }, + new Artist { Name = "Eminem" }, + new Artist { Name = "English Concert & Trevor Pinnock" }, + new Artist { Name = "Enya" }, + new Artist { Name = "Epica" }, + new Artist { Name = "Eric B. and Rakim" }, + new Artist { Name = "Eric Clapton" }, + new Artist { Name = "Eugene Ormandy" }, + new Artist { Name = "Faith No More" }, + new Artist { Name = "Falamansa" }, + new Artist { Name = "Filter" }, + new Artist { Name = "Foo Fighters" }, + new Artist { Name = "Four Tet" }, + new Artist { Name = "Frank Zappa & Captain Beefheart" }, + new Artist { Name = "Fretwork" }, + new Artist { Name = "Funk Como Le Gusta" }, + new Artist { Name = "Garbage" }, + new Artist { Name = "Gerald Moore" }, + new Artist { Name = "Gilberto Gil" }, + new Artist { Name = "Godsmack" }, + new Artist { Name = "Gonzaguinha" }, + new Artist { Name = "Göteborgs Symfoniker & Neeme Järvi" }, + new Artist { Name = "Guns N' Roses" }, + new Artist { Name = "Gustav Mahler" }, + new Artist { Name = "In This Moment" }, + new Artist { Name = "Incognito" }, + new Artist { Name = "INXS" }, + new Artist { Name = "Iron Maiden" }, + new Artist { Name = "Jagjit Singh" }, + new Artist { Name = "James Levine" }, + new Artist { Name = "Jamiroquai" }, + new Artist { Name = "Jimi Hendrix" }, + new Artist { Name = "Jimmy Buffett" }, + new Artist { Name = "Jimmy Smith" }, + new Artist { Name = "Joe Satriani" }, + new Artist { Name = "John Digweed" }, + new Artist { Name = "John Mayer" }, + new Artist { Name = "Jorge Ben" }, + new Artist { Name = "Jota Quest" }, + new Artist { Name = "Journey" }, + new Artist { Name = "Judas Priest" }, + new Artist { Name = "Julian Bream" }, + new Artist { Name = "Justice" }, + new Artist { Name = "Orchestre de l'Opéra de Lyon" }, + new Artist { Name = "King Crimson" }, + new Artist { Name = "Kiss" }, + new Artist { Name = "LCD Soundsystem" }, + new Artist { Name = "Le Tigre" }, + new Artist { Name = "Led Zeppelin" }, + new Artist { Name = "Legião Urbana" }, + new Artist { Name = "Lenny Kravitz" }, + new Artist { Name = "Les Arts Florissants & William Christie" }, + new Artist { Name = "Limp Bizkit" }, + new Artist { Name = "Linkin Park" }, + new Artist { Name = "Live" }, + new Artist { Name = "Lokua Kanza" }, + new Artist { Name = "London Symphony Orchestra" }, + new Artist { Name = "Los Tigres del Norte" }, + new Artist { Name = "Luciana Souza/Romero Lubambo" }, + new Artist { Name = "Lulu Santos" }, + new Artist { Name = "Lura" }, + new Artist { Name = "Marcos Valle" }, + new Artist { Name = "Marillion" }, + new Artist { Name = "Marisa Monte" }, + new Artist { Name = "Mark Knopfler" }, + new Artist { Name = "Martin Roscoe" }, + new Artist { Name = "Massive Attack" }, + new Artist { Name = "Maurizio Pollini" }, + new Artist { Name = "Megadeth" }, + new Artist { Name = "Mela Tenenbaum, Pro Musica Prague & Richard Kapp" }, + new Artist { Name = "Melanie Fiona" }, + new Artist { Name = "Men At Work" }, + new Artist { Name = "Metallica" }, + new Artist { Name = "M-Flo" }, + new Artist { Name = "Michael Bolton" }, + new Artist { Name = "Michael Tilson Thomas" }, + new Artist { Name = "Miles Davis" }, + new Artist { Name = "Milton Nascimento" }, + new Artist { Name = "Mobile" }, + new Artist { Name = "Modest Mouse" }, + new Artist { Name = "Mötley Crüe" }, + new Artist { Name = "Motörhead" }, + new Artist { Name = "Mumford & Sons" }, + new Artist { Name = "Munkle" }, + new Artist { Name = "Nash Ensemble" }, + new Artist { Name = "Neil Young" }, + new Artist { Name = "New York Dolls" }, + new Artist { Name = "Nick Cave and the Bad Seeds" }, + new Artist { Name = "Nicolaus Esterhazy Sinfonia" }, + new Artist { Name = "Nine Inch Nails" }, + new Artist { Name = "Nirvana" }, + new Artist { Name = "Norah Jones" }, + new Artist { Name = "Nujabes" }, + new Artist { Name = "O Terço" }, + new Artist { Name = "Oasis" }, + new Artist { Name = "Olodum" }, + new Artist { Name = "Opeth" }, + new Artist { Name = "Orchestra of The Age of Enlightenment" }, + new Artist { Name = "Os Paralamas Do Sucesso" }, + new Artist { Name = "Ozzy Osbourne" }, + new Artist { Name = "Paddy Casey" }, + new Artist { Name = "Page & Plant" }, + new Artist { Name = "Papa Wemba" }, + new Artist { Name = "Paul D'Ianno" }, + new Artist { Name = "Paul Oakenfold" }, + new Artist { Name = "Paul Van Dyk" }, + new Artist { Name = "Pearl Jam" }, + new Artist { Name = "Pet Shop Boys" }, + new Artist { Name = "Pink Floyd" }, + new Artist { Name = "Plug" }, + new Artist { Name = "Porcupine Tree" }, + new Artist { Name = "Portishead" }, + new Artist { Name = "Prince" }, + new Artist { Name = "Projected" }, + new Artist { Name = "PSY" }, + new Artist { Name = "Public Enemy" }, + new Artist { Name = "Queen" }, + new Artist { Name = "Queensrÿche" }, + new Artist { Name = "R.E.M." }, + new Artist { Name = "Radiohead" }, + new Artist { Name = "Rancid" }, + new Artist { Name = "Raul Seixas" }, + new Artist { Name = "Raunchy" }, + new Artist { Name = "Red Hot Chili Peppers" }, + new Artist { Name = "Rick Ross" }, + new Artist { Name = "Robert James" }, + new Artist { Name = "London Classical Players" }, + new Artist { Name = "Royal Philharmonic Orchestra" }, + new Artist { Name = "Run DMC" }, + new Artist { Name = "Rush" }, + new Artist { Name = "Santana" }, + new Artist { Name = "Sara Tavares" }, + new Artist { Name = "Sarah Brightman" }, + new Artist { Name = "Sasha" }, + new Artist { Name = "Scholars Baroque Ensemble" }, + new Artist { Name = "Scorpions" }, + new Artist { Name = "Sergei Prokofiev & Yuri Temirkanov" }, + new Artist { Name = "Sheryl Crow" }, + new Artist { Name = "Sir Georg Solti & Wiener Philharmoniker" }, + new Artist { Name = "Skank" }, + new Artist { Name = "Skrillex" }, + new Artist { Name = "Slash" }, + new Artist { Name = "Slayer" }, + new Artist { Name = "Soul-Junk" }, + new Artist { Name = "Soundgarden" }, + new Artist { Name = "Spyro Gyra" }, + new Artist { Name = "Stevie Ray Vaughan & Double Trouble" }, + new Artist { Name = "Stevie Ray Vaughan" }, + new Artist { Name = "Sting" }, + new Artist { Name = "Stone Temple Pilots" }, + new Artist { Name = "Styx" }, + new Artist { Name = "Sufjan Stevens" }, + new Artist { Name = "Supreme Beings of Leisure" }, + new Artist { Name = "System Of A Down" }, + new Artist { Name = "T&N" }, + new Artist { Name = "Talking Heads" }, + new Artist { Name = "Tears For Fears" }, + new Artist { Name = "Ted Nugent" }, + new Artist { Name = "Temple of the Dog" }, + new Artist { Name = "Terry Bozzio, Tony Levin & Steve Stevens" }, + new Artist { Name = "The 12 Cellists of The Berlin Philharmonic" }, + new Artist { Name = "The Axis of Awesome" }, + new Artist { Name = "The Beatles" }, + new Artist { Name = "The Black Crowes" }, + new Artist { Name = "The Black Keys" }, + new Artist { Name = "The Carpenters" }, + new Artist { Name = "The Cat Empire" }, + new Artist { Name = "The Cult" }, + new Artist { Name = "The Cure" }, + new Artist { Name = "The Decemberists" }, + new Artist { Name = "The Doors" }, + new Artist { Name = "The Eagles of Death Metal" }, + new Artist { Name = "The Go! Team" }, + new Artist { Name = "The Head and the Heart" }, + new Artist { Name = "The Jezabels" }, + new Artist { Name = "The King's Singers" }, + new Artist { Name = "The Lumineers" }, + new Artist { Name = "The Offspring" }, + new Artist { Name = "The Police" }, + new Artist { Name = "The Posies" }, + new Artist { Name = "The Prodigy" }, + new Artist { Name = "The Rolling Stones" }, + new Artist { Name = "The Rubberbandits" }, + new Artist { Name = "The Smashing Pumpkins" }, + new Artist { Name = "The Stone Roses" }, + new Artist { Name = "The Who" }, + new Artist { Name = "Them Crooked Vultures" }, + new Artist { Name = "TheStart" }, + new Artist { Name = "Thievery Corporation" }, + new Artist { Name = "Tiësto" }, + new Artist { Name = "Tim Maia" }, + new Artist { Name = "Ton Koopman" }, + new Artist { Name = "Tool" }, + new Artist { Name = "Tori Amos" }, + new Artist { Name = "Trampled By Turtles" }, + new Artist { Name = "Trans-Siberian Orchestra" }, + new Artist { Name = "Tygers of Pan Tang" }, + new Artist { Name = "U2" }, + new Artist { Name = "UB40" }, + new Artist { Name = "Uh Huh Her " }, + new Artist { Name = "Van Halen" }, + new Artist { Name = "Various Artists" }, + new Artist { Name = "Velvet Revolver" }, + new Artist { Name = "Venus Hum" }, + new Artist { Name = "Vicente Fernandez" }, + new Artist { Name = "Vinícius De Moraes" }, + new Artist { Name = "Weezer" }, + new Artist { Name = "Weird Al" }, + new Artist { Name = "Wendy Carlos" }, + new Artist { Name = "Wilhelm Kempff" }, + new Artist { Name = "Yano" }, + new Artist { Name = "Yehudi Menuhin" }, + new Artist { Name = "Yes" }, + new Artist { Name = "Yo-Yo Ma" }, + new Artist { Name = "Zeca Pagodinho" }, + new Artist { Name = "אריק אינשטיין" } + }; + + context.Artists.AddRange(artists); + + return artists; + } + + private static List AddGenres(MusicStoreEntities context) + { + var genres = new List + { + new Genre { Name = "Pop" }, + new Genre { Name = "Rock" }, + new Genre { Name = "Jazz" }, + new Genre { Name = "Metal" }, + new Genre { Name = "Electronic" }, + new Genre { Name = "Blues" }, + new Genre { Name = "Latin" }, + new Genre { Name = "Rap" }, + new Genre { Name = "Classical" }, + new Genre { Name = "Alternative" }, + new Genre { Name = "Country" }, + new Genre { Name = "R&B" }, + new Genre { Name = "Indie" }, + new Genre { Name = "Punk" }, + new Genre { Name = "World" } + }; + + context.Genres.AddRange(genres); + + return genres; + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Models/ShoppingCart.cs b/src/MvcMusicStore.Spa/Models/ShoppingCart.cs new file mode 100644 index 0000000000..cab45712fe --- /dev/null +++ b/src/MvcMusicStore.Spa/Models/ShoppingCart.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; + +namespace MvcMusicStore.Models +{ + public class ShoppingCart + { + public const string CartSessionKey = "CartId"; + + private readonly MusicStoreEntities _storeContext; + private readonly string _cartId; + + private ShoppingCart(MusicStoreEntities storeContext, string cartId) + { + _storeContext = storeContext; + _cartId = cartId; + } + + public static ShoppingCart GetCart(MusicStoreEntities storeContext, Controller controller) + { + return new ShoppingCart(storeContext, GetCartId(controller.HttpContext)); + } + + private static string GetCartId(HttpContextBase context) + { + if (context.Session[CartSessionKey] == null) + { + var username = context.User.Identity.Name; + + context.Session[CartSessionKey] = !string.IsNullOrWhiteSpace(username) + ? username + : Guid.NewGuid().ToString(); + } + + return context.Session[CartSessionKey].ToString(); + } + + public async Task AddToCart(Album album) + { + var cartItem = await GetCartItem(album.AlbumId); + + if (cartItem == null) + { + cartItem = new Cart + { + AlbumId = album.AlbumId, + CartId = _cartId, + Count = 1, + DateCreated = DateTime.Now + }; + + _storeContext.Carts.Add(cartItem); + } + else + { + cartItem.Count++; + } + } + + public async Task RemoveFromCart(int id) + { + var cartItem = await GetCartItem(id); + + if (cartItem != null) + { + if (cartItem.Count > 1) + { + return --cartItem.Count; + } + + _storeContext.Carts.Remove(cartItem); + } + + return 0; + } + + private Task GetCartItem(int albumId) + { + return _storeContext.Carts.SingleOrDefaultAsync( + c => c.CartId == _cartId && c.AlbumId == albumId); + } + + public IQueryable GetCartItems() + { + return _storeContext.Carts.Where(c => c.CartId == _cartId); + } + + public Task GetCount() + { + return _storeContext.Carts + .Where(c => c.CartId == _cartId) + .Select(c => c.Count) + .SumAsync(); + } + + public Task GetTotal() + { + return _storeContext.Carts + .Where(c => c.CartId == _cartId) + .Select(c => c.Count * c.Album.Price) + .SumAsync(); + } + + public async Task CreateOrder(Order order) + { + decimal orderTotal = 0; + + var cartItems = await _storeContext.Carts + .Where(c => c.CartId == _cartId) + .Include(c => c.Album) + .ToListAsync(); + + foreach (var item in cartItems) + { + order.OrderDetails.Add(new OrderDetail + { + AlbumId = item.AlbumId, + OrderId = order.OrderId, + UnitPrice = item.Album.Price, + Quantity = item.Count, + }); + + orderTotal += item.Count * item.Album.Price; + } + + order.Total = orderTotal; + + await EmptyCart(); + + return order.OrderId; + } + + private async Task EmptyCart() + { + foreach (var cartItem in await _storeContext.Carts.Where( + c => c.CartId == _cartId).ToListAsync()) + { + _storeContext.Carts.Remove(cartItem); + } + } + + public async Task MigrateCart(string userName) + { + var carts = await _storeContext.Carts.Where(c => c.CartId == _cartId).ToListAsync(); + + foreach (var item in carts) + { + item.CartId = userName; + } + + await _storeContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj b/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj new file mode 100644 index 0000000000..03a8234ad0 --- /dev/null +++ b/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj @@ -0,0 +1,330 @@ + + + + + + Debug + AnyCPU + + + 2.0 + {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + MvcMusicStore + MvcMusicStore + v4.5 + false + true + + + + + 1.0 + False + public/js + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + False + ..\..\packages\EntityFramework.6.0.2\lib\net45\EntityFramework.dll + + + False + ..\..\packages\EntityFramework.6.0.2\lib\net45\EntityFramework.SqlServer.dll + + + ..\..\packages\Microsoft.AspNet.Identity.Core.1.0.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\..\packages\Microsoft.AspNet.Identity.EntityFramework.1.0.0\lib\net45\Microsoft.AspNet.Identity.EntityFramework.dll + + + ..\..\packages\Microsoft.AspNet.Identity.Owin.1.0.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + + ..\..\packages\Microsoft.Owin.2.0.2\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Microsoft.Owin.Host.SystemWeb.2.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\..\packages\Microsoft.Owin.Security.2.0.2\lib\net45\Microsoft.Owin.Security.dll + + + ..\..\packages\Microsoft.Owin.Security.Cookies.2.0.2\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\..\packages\Microsoft.Owin.Security.Facebook.2.0.0\lib\net45\Microsoft.Owin.Security.Facebook.dll + + + ..\..\packages\Microsoft.Owin.Security.Google.2.0.0\lib\net45\Microsoft.Owin.Security.Google.dll + + + ..\..\packages\Microsoft.Owin.Security.MicrosoftAccount.2.0.0\lib\net45\Microsoft.Owin.Security.MicrosoftAccount.dll + + + ..\..\packages\Microsoft.Owin.Security.OAuth.2.0.2\lib\net45\Microsoft.Owin.Security.OAuth.dll + + + ..\..\packages\Microsoft.Owin.Security.Twitter.2.0.0\lib\net45\Microsoft.Owin.Security.Twitter.dll + + + True + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + ..\..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + + + + + + + + + + + + + False + ..\..\packages\Microsoft.AspNet.WebPages.3.1.1\lib\net45\System.Web.Helpers.dll + + + False + ..\..\packages\Microsoft.AspNet.Mvc.5.1.1\lib\net45\System.Web.Mvc.dll + + + False + ..\..\packages\Microsoft.AspNet.Razor.3.1.1\lib\net45\System.Web.Razor.dll + + + False + ..\..\packages\Microsoft.AspNet.WebPages.3.1.1\lib\net45\System.Web.WebPages.dll + + + False + ..\..\packages\Microsoft.AspNet.WebPages.3.1.1\lib\net45\System.Web.WebPages.Deployment.dll + + + False + ..\..\packages\Microsoft.AspNet.WebPages.3.1.1\lib\net45\System.Web.WebPages.Razor.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Global.asax + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GenreMenu.html + + + + + + + + + + + + + + + + + + + + + Designer + + + Web.config + + + Web.config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + False + amd + + + + + + + + + + + + True + True + 43524 + / + http://localhost:43524/ + False + False + + + False + + + + + + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Properties/AssemblyInfo.cs b/src/MvcMusicStore.Spa/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..51fbcae968 --- /dev/null +++ b/src/MvcMusicStore.Spa/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MvcMusicStore")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MvcMusicStore")] +[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("64547e1b-3030-4458-ab71-a970f2916ed6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/MvcMusicStore.Spa/Startup.cs b/src/MvcMusicStore.Spa/Startup.cs new file mode 100644 index 0000000000..55c2068af4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Startup.cs @@ -0,0 +1,17 @@ +using Microsoft.Owin; +using Owin; + +[assembly: OwinStartupAttribute(typeof(MvcMusicStore.Startup))] + +namespace MvcMusicStore +{ + public partial class Startup + { + public void Configuration(IAppBuilder app) + { + ConfigureAuth(app); + + ConfigureApp(app); + } + } +} diff --git a/src/MvcMusicStore.Spa/ViewModels/ShoppingCartRemoveViewModel.cs b/src/MvcMusicStore.Spa/ViewModels/ShoppingCartRemoveViewModel.cs new file mode 100644 index 0000000000..b190275d44 --- /dev/null +++ b/src/MvcMusicStore.Spa/ViewModels/ShoppingCartRemoveViewModel.cs @@ -0,0 +1,11 @@ +namespace MvcMusicStore.ViewModels +{ + public class ShoppingCartRemoveViewModel + { + public string Message { get; set; } + public decimal CartTotal { get; set; } + public int CartCount { get; set; } + public int ItemCount { get; set; } + public int DeleteId { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/ViewModels/ShoppingCartViewModel.cs b/src/MvcMusicStore.Spa/ViewModels/ShoppingCartViewModel.cs new file mode 100644 index 0000000000..53702aeb81 --- /dev/null +++ b/src/MvcMusicStore.Spa/ViewModels/ShoppingCartViewModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using MvcMusicStore.Models; + +namespace MvcMusicStore.ViewModels +{ + public class ShoppingCartViewModel + { + public List CartItems { get; set; } + public decimal CartTotal { get; set; } + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Account/ExternalLoginConfirmation.cshtml b/src/MvcMusicStore.Spa/Views/Account/ExternalLoginConfirmation.cshtml new file mode 100644 index 0000000000..441dbf7d99 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/ExternalLoginConfirmation.cshtml @@ -0,0 +1,36 @@ +@model MvcMusicStore.Models.ExternalLoginConfirmationViewModel +@{ + ViewBag.Title = "Register"; +} +

@ViewBag.Title.

+

Associate your @ViewBag.LoginProvider account.

+ +@using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + +

Association Form

+
+ @Html.ValidationSummary(true) +

+ You've successfully authenticated with @ViewBag.LoginProvider. + Please enter a user name for this site below and click the Register button to finish + logging in. +

+
+ @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) +
+ @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) + @Html.ValidationMessageFor(m => m.UserName) +
+
+
+
+ +
+
+} + +@section Scripts { + @Scripts.Render("~/bundles/jqueryval") +} diff --git a/src/MvcMusicStore.Spa/Views/Account/ExternalLoginFailure.cshtml b/src/MvcMusicStore.Spa/Views/Account/ExternalLoginFailure.cshtml new file mode 100644 index 0000000000..342eb693d6 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/ExternalLoginFailure.cshtml @@ -0,0 +1,6 @@ +@{ + ViewBag.Title = "Login Failure"; +} + +

@ViewBag.Title.

+

Unsuccessful login with service.

diff --git a/src/MvcMusicStore.Spa/Views/Account/Login.cshtml b/src/MvcMusicStore.Spa/Views/Account/Login.cshtml new file mode 100644 index 0000000000..8c50e067fb --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/Login.cshtml @@ -0,0 +1,100 @@ +@model MvcMusicStore.Models.LoginViewModel + +@{ + ViewBag.Title = "Log in"; + ViewBag.ngApp = "MusicStore.Store"; +} + +@section NavBarItems { + +
  • +@Html.InlineData("GenreMenuList", "GenresApi") + +} + +

    @ViewBag.Title.

    + +
    +
    +
    + @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, + new { @class = "form-horizontal", role = "form", novalidate = "", name = "login", + app_prevent_submit = "login.$invalid", + ng_submit = "login.submitAttempted=true" })) + { + @Html.AntiForgeryToken() +

    Use a local account to log in.

    +
    + @Html.ValidationSummary(true) + +
    + @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngTextBoxFor(m => m.UserName, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.UserName, "login", new { @class = "help-block field-validation-error" }) +
    +
    + + @* What this might look like using Tag Helpers: + <@div class="form-group" validation-for="UserName" validation-form-name="login" validation-class="has-error"> + <@label for="UserName" class="col-md-2 control-label"> +
    + <@input for="UserName" class="form-control" /> + <@span validation-for="UserName" validation-form-name="login" class="field-validation-error"> +
    + + *@ + +
    + @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.Password, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.Password, "login", new { @class = "help-block field-validation-error" }) +
    +
    + +
    +
    +
    + @Html.CheckBoxFor(m => m.RememberMe) + @Html.LabelFor(m => m.RememberMe) +
    +
    +
    + +
    +
    + +
    +
    + +

    + @Html.ActionLink("Register", "Register") if you don't have a local account. +

    + } +
    +
    +
    +
    + @Html.Partial("_ExternalLoginsListPartial", new { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl }) +
    +
    +
    + +@section Scripts { + + + + @* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Account/Manage.cshtml b/src/MvcMusicStore.Spa/Views/Account/Manage.cshtml new file mode 100644 index 0000000000..81781bc5dc --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/Manage.cshtml @@ -0,0 +1,39 @@ +@using MvcMusicStore.Models; +@using Microsoft.AspNet.Identity; + +@{ + ViewBag.Title = "Manage Account"; + ViewBag.ngApp = "MusicStore.Store"; +} + +

    @ViewBag.Title.

    + +

    @ViewBag.StatusMessage

    + +
    +
    + @if (ViewBag.HasLocalPassword) + { + @Html.Partial("_ChangePasswordPartial") + } + else + { + @Html.Partial("_SetPasswordPartial") + } + +
    + @Html.Action("RemoveAccountList") + @Html.Partial("_ExternalLoginsListPartial", new { Action = "LinkLogin", ReturnUrl = ViewBag.ReturnUrl }) +
    +
    +
    + +@section Scripts { + + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} diff --git a/src/MvcMusicStore.Spa/Views/Account/Register.cshtml b/src/MvcMusicStore.Spa/Views/Account/Register.cshtml new file mode 100644 index 0000000000..04ac6e0d12 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/Register.cshtml @@ -0,0 +1,78 @@ +@model MvcMusicStore.Models.RegisterViewModel + +@{ + ViewBag.Title = "Register"; + ViewBag.ngApp = "MusicStore.Store"; +} + +@section NavBarItems { + +
  • +@Html.InlineData("GenreMenuList", "GenresApi") + +} + +

    @ViewBag.Title.

    + +@using (Html.BeginForm("Register", "Account", FormMethod.Post, + new { @class = "form-horizontal", role = "form", novalidate = "", name = "register", + app_prevent_submit = "register.$invalid", + ng_submit = "register.submitAttempted=true" })) +{ + @Html.AntiForgeryToken() +

    Create a new account.

    +
    + @Html.ValidationSummary() + +
    + @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngTextBoxFor(m => m.UserName, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.UserName, "register", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.Password, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.Password, "register", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.ConfirmPassword, "register", new { @class = "help-block field-validation-error" }) +
    +
    + +
    +
    + +
    +
    +} + +@section Scripts { + + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} diff --git a/src/MvcMusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml b/src/MvcMusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml new file mode 100644 index 0000000000..26a2ab89e8 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml @@ -0,0 +1,58 @@ +@using Microsoft.AspNet.Identity + +@model MvcMusicStore.Models.ManageUserViewModel + +

    You're logged in as @User.Identity.GetUserName().

    + +@using (Html.BeginForm("Manage", "Account", FormMethod.Post, + new { @class = "form-horizontal", role = "form", novalidate = "", name = "changePassword", + app_prevent_submit = "changePassword.$invalid", + ng_submit = "changePassword.submitAttempted=true" })) +{ + @Html.AntiForgeryToken() +

    Change Password

    +
    + @Html.ValidationSummary() + +
    + @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.OldPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.OldPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.NewPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.NewPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.ConfirmPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    +
    + +
    +
    +} diff --git a/src/MvcMusicStore.Spa/Views/Account/_ExternalLoginsListPartial.cshtml b/src/MvcMusicStore.Spa/Views/Account/_ExternalLoginsListPartial.cshtml new file mode 100644 index 0000000000..529e4c5f74 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/_ExternalLoginsListPartial.cshtml @@ -0,0 +1,31 @@ +@using Microsoft.Owin.Security + +

    Use another service to log in.

    +
    +@{ + var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); + if (loginProviders.Count() == 0) + { +
    +

    There are no external authentication services configured. See this article + for details on setting up this ASP.NET application to support logging in via external services.

    +
    + } + else + { + string action = Model.Action; + string returnUrl = Model.ReturnUrl; + using (Html.BeginForm(action, "Account", new { ReturnUrl = returnUrl })) + { + @Html.AntiForgeryToken() +
    +

    + @foreach (AuthenticationDescription p in loginProviders) + { + + } +

    +
    + } + } +} diff --git a/src/MvcMusicStore.Spa/Views/Account/_RemoveAccountPartial.cshtml b/src/MvcMusicStore.Spa/Views/Account/_RemoveAccountPartial.cshtml new file mode 100644 index 0000000000..b4c80813bf --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/_RemoveAccountPartial.cshtml @@ -0,0 +1,34 @@ +@model ICollection + +@if (Model.Count > 0) +{ +

    Registered Logins

    + + + @foreach (var account in Model) + { + + + + + } + +
    @account.LoginProvider + @if (ViewBag.ShowRemoveButton) + { + using (Html.BeginForm("Disassociate", "Account")) + { + @Html.AntiForgeryToken() +
    + @Html.Hidden("loginProvider", account.LoginProvider) + @Html.Hidden("providerKey", account.ProviderKey) + +
    + } + } + else + { + @:   + } +
    +} diff --git a/src/MvcMusicStore.Spa/Views/Account/_SetPasswordPartial.cshtml b/src/MvcMusicStore.Spa/Views/Account/_SetPasswordPartial.cshtml new file mode 100644 index 0000000000..4f23219229 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Account/_SetPasswordPartial.cshtml @@ -0,0 +1,32 @@ +@model MvcMusicStore.Models.ManageUserViewModel + +

    + You do not have a local username/password for this site. Add a local + account so you can log in without an external login. +

    + +@using (Html.BeginForm("Manage", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() + +

    Create Local Login

    +
    + @Html.ValidationSummary() +
    + @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) +
    + @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) +
    +
    +
    + @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
    + @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
    +
    +
    +
    + +
    +
    +} diff --git a/src/MvcMusicStore.Spa/Views/Checkout/AddressAndPayment.cshtml b/src/MvcMusicStore.Spa/Views/Checkout/AddressAndPayment.cshtml new file mode 100644 index 0000000000..d39b9e916e --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Checkout/AddressAndPayment.cshtml @@ -0,0 +1,32 @@ +@model MvcMusicStore.Models.Order + +@{ + ViewBag.Title = "Address And Payment"; +} + +@section Scripts { + @Scripts.Render("~/bundles/jqueryval") +} + +@using (Html.BeginForm()) { + +

    Address And Payment

    +
    + Shipping Information + + @Html.EditorForModel() +
    +
    + Payment +

    We're running a promotion: all music is free with the promo code: "FREE"

    + +
    + @Html.Label("Promo Code") +
    +
    + @Html.TextBox("PromoCode") +
    +
    + + +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Checkout/Complete.cshtml b/src/MvcMusicStore.Spa/Views/Checkout/Complete.cshtml new file mode 100644 index 0000000000..d34855382e --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Checkout/Complete.cshtml @@ -0,0 +1,13 @@ +@model int + +@{ + ViewBag.Title = "Checkout Complete"; +} + +

    Checkout Complete

    + +

    Thanks for your order! Your order number is: @Model

    + +

    How about shopping for some more music in our + @Html.ActionLink("Store", "Index", "Home") +

    \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Home/Index.cshtml b/src/MvcMusicStore.Spa/Views/Home/Index.cshtml new file mode 100644 index 0000000000..d2fbbaa025 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Home/Index.cshtml @@ -0,0 +1,25 @@ +@{ + ViewBag.Title = "Home Page"; + ViewBag.ngApp = "MusicStore.Store"; +} + +@section NavBarItems { + +
  • +@Html.InlineData("GenreMenuList", "GenresApi") + +} + +
    + +@Html.InlineData("MostPopular", "AlbumsApi") + +@section Scripts { + + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Shared/Error.cshtml b/src/MvcMusicStore.Spa/Views/Shared/Error.cshtml new file mode 100644 index 0000000000..be55b17d00 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Shared/Error.cshtml @@ -0,0 +1,9 @@ +@model System.Web.Mvc.HandleErrorInfo + +@{ + ViewBag.Title = "Error"; +} + +

    Error.

    +

    An error occurred while processing your request.

    + diff --git a/src/MvcMusicStore.Spa/Views/Shared/_Layout.cshtml b/src/MvcMusicStore.Spa/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..4dc091fef9 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Shared/_Layout.cshtml @@ -0,0 +1,51 @@ + + + + + + @ViewBag.Title – MVC Music Store + + + + + + +
    + @RenderBody() +
    + +
    + + @* TODO: Need to figure out best way to switch these to min links for release, e.g. new helper, + Grunt task to replace, CDN support, etc. *@ + + @**@ + + + @RenderSection("scripts", required: false) + + diff --git a/src/MvcMusicStore.Spa/Views/Shared/_LoginPartial.cshtml b/src/MvcMusicStore.Spa/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000000..db95f4ae3f --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,45 @@ +@using Microsoft.AspNet.Identity + +@{ + Func js = input => Html.Raw(HttpUtility.JavaScriptStringEncode(input, false)); +} + +@if (Request.IsAuthenticated) +{ + using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) + { + @Html.AntiForgeryToken() + + + + @Html.Json(new { + isAuthenticated = true, + userName = User.Identity.GetUserName(), + userId = User.Identity.GetUserId(), + roles = ((System.Security.Claims.ClaimsPrincipal)User).Claims + .Where(c => c.Type == System.Security.Claims.ClaimTypes.Role) + .Select(role => role.Value) + }, + new { id = "userDetails" }) + } +} +else +{ + + + @Html.Json(new { + isAuthenticated = false, + userName = (string)null, + userId = (string)null, + roles = Enumerable.Empty() + }, + new { id = "userDetails" }) +} diff --git a/src/MvcMusicStore.Spa/Views/ShoppingCart/CartSummary.cshtml b/src/MvcMusicStore.Spa/Views/ShoppingCart/CartSummary.cshtml new file mode 100644 index 0000000000..eae797ebd4 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/ShoppingCart/CartSummary.cshtml @@ -0,0 +1,9 @@ +@if (ViewBag.CartCount > 0) +{ +
  • + + + @ViewBag.CartCount + +
  • +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/ShoppingCart/Index.cshtml b/src/MvcMusicStore.Spa/Views/ShoppingCart/Index.cshtml new file mode 100644 index 0000000000..9e757a8a42 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/ShoppingCart/Index.cshtml @@ -0,0 +1,91 @@ +@model MvcMusicStore.ViewModels.ShoppingCartViewModel +@{ + ViewBag.Title = "Shopping Cart"; +} + +@section Scripts { + +} + +

    + Review your cart: +

    +

    + @Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout") +

    +
    +
    + + + + + + + + @foreach (var item in Model.CartItems) + { + + + + + + + } + + + + + + +
    + Album Name + + Price (each) + + Quantity +
    + @Html.ActionLink(item.Album.Title, + "Details", "Store", new { id = item.AlbumId }, null) + + @item.Album.Price + + @item.Count + + + Remove from cart +
    + Total + + + + @Model.CartTotal +
    \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/StoreManager/Create.cshtml b/src/MvcMusicStore.Spa/Views/StoreManager/Create.cshtml new file mode 100644 index 0000000000..d338585cf7 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/StoreManager/Create.cshtml @@ -0,0 +1,72 @@ +@model MvcMusicStore.Models.Album + +@{ + ViewBag.Title = "Create"; +} + +

    Create

    + +@using (Html.BeginForm()) +{ + @Html.AntiForgeryToken() + +
    +

    Album

    +
    + @Html.ValidationSummary(true) + +
    + @Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" }) +
    + @Html.DropDownList("GenreId", String.Empty) + @Html.ValidationMessageFor(model => model.GenreId) +
    +
    + +
    + @Html.LabelFor(model => model.ArtistId, "ArtistId", new { @class = "control-label col-md-2" }) +
    + @Html.DropDownList("ArtistId", String.Empty) + @Html.ValidationMessageFor(model => model.ArtistId) +
    +
    + +
    + @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" }) +
    + @Html.EditorFor(model => model.Title) + @Html.ValidationMessageFor(model => model.Title) +
    +
    + +
    + @Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" }) +
    + @Html.EditorFor(model => model.Price) + @Html.ValidationMessageFor(model => model.Price) +
    +
    + +
    + @Html.LabelFor(model => model.AlbumArtUrl, new { @class = "control-label col-md-2" }) +
    + @Html.EditorFor(model => model.AlbumArtUrl) + @Html.ValidationMessageFor(model => model.AlbumArtUrl) +
    +
    + +
    +
    + +
    +
    +
    +} + +
    + @Html.ActionLink("Back to List", "Index") +
    + +@section Scripts { + @Scripts.Render("~/bundles/jqueryval") +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/StoreManager/Delete.cshtml b/src/MvcMusicStore.Spa/Views/StoreManager/Delete.cshtml new file mode 100644 index 0000000000..df5434633a --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/StoreManager/Delete.cshtml @@ -0,0 +1,23 @@ +@model MvcMusicStore.Models.Album + +@{ + ViewBag.Title = "Delete"; +} + +

    Delete Confirmation

    + +

    + Are you sure you want to delete the album titled + @Model.Title? +

    + +@using (Html.BeginForm()) +{ +

    + +

    +

    + @Html.ActionLink("Back to List", "Index") +

    + +} diff --git a/src/MvcMusicStore.Spa/Views/StoreManager/Index.cshtml b/src/MvcMusicStore.Spa/Views/StoreManager/Index.cshtml new file mode 100644 index 0000000000..1839003730 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/StoreManager/Index.cshtml @@ -0,0 +1,25 @@ +@model IEnumerable + +@{ + ViewBag.Title = "Index"; + ViewBag.ngApp = "MusicStore.Admin"; +} + +

    Store Manager

    + +
    + +@Html.InlineData("Lookup", "ArtistsApi") +@Html.InlineData("Lookup", "GenresApi") + +@section Scripts { + + + + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/Views/Web.config b/src/MvcMusicStore.Spa/Views/Web.config new file mode 100644 index 0000000000..4bc2a4b7f2 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/Web.config @@ -0,0 +1,34 @@ + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Views/_ViewStart.cshtml b/src/MvcMusicStore.Spa/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..2de62418c0 --- /dev/null +++ b/src/MvcMusicStore.Spa/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Views/Shared/_Layout.cshtml"; +} diff --git a/src/MvcMusicStore.Spa/Web.Debug.config b/src/MvcMusicStore.Spa/Web.Debug.config new file mode 100644 index 0000000000..c1bf1db285 --- /dev/null +++ b/src/MvcMusicStore.Spa/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Web.Release.config b/src/MvcMusicStore.Spa/Web.Release.config new file mode 100644 index 0000000000..bdd237c337 --- /dev/null +++ b/src/MvcMusicStore.Spa/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + diff --git a/src/MvcMusicStore.Spa/Web.config b/src/MvcMusicStore.Spa/Web.config new file mode 100644 index 0000000000..b95952aba1 --- /dev/null +++ b/src/MvcMusicStore.Spa/Web.config @@ -0,0 +1,129 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/bower.json b/src/MvcMusicStore.Spa/bower.json new file mode 100644 index 0000000000..224510d8ae --- /dev/null +++ b/src/MvcMusicStore.Spa/bower.json @@ -0,0 +1,17 @@ +{ + "name": "MvcMusicStore", + "version": "0.0.0", + "private": true, + "dependencies": { + "bootstrap": "~3.1.0", + "jquery.validation": "~1.11.1", + "jquery": "~2.1.0", + "modernizr": "~2.7.1", + "respond": "~1.4.2", + "dt-angular": "~1.2.15", + "angular": "~1.2.15", + "angular-route": "~1.2.15", + "angular-bootstrap": "~0.10.0", + "dt-angular-ui-bootstrap": "*" + } +} diff --git a/src/MvcMusicStore.Spa/package.json b/src/MvcMusicStore.Spa/package.json new file mode 100644 index 0000000000..19f18975b1 --- /dev/null +++ b/src/MvcMusicStore.Spa/package.json @@ -0,0 +1,16 @@ +{ + "name": "MvcMusicStore", + "version": "0.0.0", + "devDependencies": { + "grunt": "~0.4.2", + "grunt-contrib-jshint": "~0.6.3", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-copy": "~0.5.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-less": "~0.9.0", + "grunt-typescript": "~0.2.8", + "grunt-tslint": "~0.4.1", + "grunt-tsng": "~0.1.3" + } +} \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/packages.config b/src/MvcMusicStore.Spa/packages.config new file mode 100644 index 0000000000..5af7250436 --- /dev/null +++ b/src/MvcMusicStore.Spa/packages.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MvcMusicStore.Spa/tslint.json b/src/MvcMusicStore.Spa/tslint.json new file mode 100644 index 0000000000..975f4ff5c5 --- /dev/null +++ b/src/MvcMusicStore.Spa/tslint.json @@ -0,0 +1,48 @@ +{ + "rules": { + "class-name": true, + "curly": true, + "eofline": false, + "forin": true, + "indent": [true, 4], + "label-position": true, + "label-undefined": true, + "max-line-length": [true, 140], + "no-arg": true, + "no-bitwise": 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": true, + "no-eval": true, + "no-string-literal": true, + "no-trailing-whitespace": true, + "no-unreachable": true, + "one-line": [true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [true, "double"], + "radix": true, + "semicolon": true, + "triple-equals": [true, "allow-null-check"], + "variable-name": false, + "whitespace": [true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file