зеркало из https://github.com/aspnet/MusicStore.git
Added new SPA port of original Music Store app
This commit is contained in:
Родитель
4c026726f6
Коммит
d4efedeb17
|
@ -29,4 +29,8 @@ nuget.exe
|
|||
build/
|
||||
*net45.csproj
|
||||
*k10.csproj
|
||||
App_Data/
|
||||
App_Data/
|
||||
bower_components
|
||||
node_modules
|
||||
*.sln.ide
|
||||
*/*.Spa/public/*
|
|
@ -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
|
||||
|
|
|
@ -73,6 +73,8 @@
|
|||
<Content Include="Views\Store\Details.cshtml" />
|
||||
<Content Include="Views\Store\Index.cshtml" />
|
||||
<Content Include="web.config" />
|
||||
<Content Include="web.Debug.config" />
|
||||
<Content Include="web.Release.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Components\CartSummaryComponent.cs" />
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 -->
|
||||
|
||||
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
|
||||
<!--
|
||||
In the example below, the "SetAttributes" transform will change the value of
|
||||
"connectionString" to use "ReleaseSQLServer" only when the "Match" locator
|
||||
finds an attribute "name" that has a value of "MyDB".
|
||||
|
||||
<connectionStrings>
|
||||
<add name="MyDB"
|
||||
connectionString="Data Source=ReleaseSQLServer;Initial Catalog=MyReleaseDB;Integrated Security=True"
|
||||
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
|
||||
</connectionStrings>
|
||||
-->
|
||||
<system.web>
|
||||
<!--
|
||||
In the example below, the "Replace" transform will replace the entire
|
||||
<customErrors> section of your web.config file.
|
||||
Note that because there is only one customErrors section under the
|
||||
<system.web> node, there is no need to use the "xdt:Locator" attribute.
|
||||
|
||||
<customErrors defaultRedirect="GenericError.htm"
|
||||
mode="RemoteOnly" xdt:Transform="Replace">
|
||||
<error statusCode="500" redirect="InternalError.htm"/>
|
||||
</customErrors>
|
||||
-->
|
||||
</system.web>
|
||||
</configuration>
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 -->
|
||||
|
||||
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
|
||||
<!--
|
||||
In the example below, the "SetAttributes" transform will change the value of
|
||||
"connectionString" to use "ReleaseSQLServer" only when the "Match" locator
|
||||
finds an attribute "name" that has a value of "MyDB".
|
||||
|
||||
<connectionStrings>
|
||||
<add name="MyDB"
|
||||
connectionString="Data Source=ReleaseSQLServer;Initial Catalog=MyReleaseDB;Integrated Security=True"
|
||||
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
|
||||
</connectionStrings>
|
||||
-->
|
||||
<system.web>
|
||||
<compilation xdt:Transform="RemoveAttributes(debug)" />
|
||||
<!--
|
||||
In the example below, the "Replace" transform will replace the entire
|
||||
<customErrors> section of your web.config file.
|
||||
Note that because there is only one customErrors section under the
|
||||
<system.web> node, there is no need to use the "xdt:Locator" attribute.
|
||||
|
||||
<customErrors defaultRedirect="GenericError.htm"
|
||||
mode="RemoteOnly" xdt:Transform="Replace">
|
||||
<error statusCode="500" redirect="InternalError.htm"/>
|
||||
</customErrors>
|
||||
-->
|
||||
</system.web>
|
||||
</configuration>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ApplicationUser>(new UserStore<ApplicationUser>(context));
|
||||
var roleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 31 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 248 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 2.9 KiB |
|
@ -0,0 +1,303 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="77.981812"
|
||||
height="38"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.2 r9819"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:export-filename="C:\Users\Jon\SkyDrive\Projects\Git\MvcMusicStore\MvcMusicStore\Content\Images\logo.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="11.313708"
|
||||
inkscape:cx="68.62794"
|
||||
inkscape:cy="21.563703"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1137"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid4175"
|
||||
empspacing="5"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-381.25,-434.47363)">
|
||||
<g
|
||||
id="g3138"
|
||||
transform="matrix(0.99433276,0,0,0.99433276,2.6879598,-28.875086)">
|
||||
<g
|
||||
transform="matrix(0.95191143,0,0,0.94999997,-13.610187,70.866904)"
|
||||
id="g4123">
|
||||
<g
|
||||
id="g4004"
|
||||
transform="translate(6.4397225,-81.443549)">
|
||||
<path
|
||||
transform="matrix(1.037037,0,0,1.0566038,33.991419,-29.473365)"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
sodipodi:ry="14.196428"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:cx="425"
|
||||
id="path3007-1"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(0.16151528,0,0,0.16456275,406.08815,432.19425)"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
sodipodi:ry="14.196428"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:cx="425"
|
||||
id="path3007-0-4"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
sodipodi:type="arc" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1.31862605;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-1"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(0.5308556,0,0,0.5308556,229.63162,198.10213)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.61952281;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-4-1"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(1.1299019,0,0,1.1299019,-54.220053,-70.262937)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.8002516;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-5-5"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(0.83035093,0,0,0.83035093,87.718968,63.932058)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.87783366px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 481.53079,442.83849 -0.0153,6.77692 c 9.55428,-0.16929 14.45853,-10.06218 12.50577,-19.31642 l -8.12484,3.61854 c 0.006,4.27743 -1.21305,6.01267 -4.23942,6.33245 z"
|
||||
id="path3916-5-2-7"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:transform-center-x="6.427012"
|
||||
inkscape:transform-center-y="17.713434" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.87783366px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 480.46352,428.30022 0.0153,-6.77692 c -9.55428,0.16929 -14.45853,10.06218 -12.50577,19.31642 l 8.12484,-3.61854 c -0.006,-4.27743 1.21305,-6.01267 4.23942,-6.33245 z"
|
||||
id="path3916-5-2-7-6"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:transform-center-x="-6.4270161"
|
||||
inkscape:transform-center-y="-17.713434" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.95191143,0,0,0.94999997,-63.034658,70.866904)"
|
||||
id="g4123-1">
|
||||
<g
|
||||
id="g4004-4"
|
||||
transform="translate(6.4397225,-81.443549)">
|
||||
<path
|
||||
transform="matrix(1.037037,0,0,1.0566038,33.991419,-29.473365)"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
sodipodi:ry="14.196428"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:cx="425"
|
||||
id="path3007-1-2"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
sodipodi:type="arc" />
|
||||
<path
|
||||
transform="matrix(0.16151528,0,0,0.16456275,406.08815,432.19425)"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
sodipodi:ry="14.196428"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:cx="425"
|
||||
id="path3007-0-4-3"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
sodipodi:type="arc" />
|
||||
</g>
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1.31862605;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-1-2"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(0.5308556,0,0,0.5308556,229.63162,198.10213)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.61952281;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-4-1-2"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(1.1299019,0,0,1.1299019,-54.220053,-70.262937)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.8002516;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-5-5-1"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(0.83035093,0,0,0.83035093,87.718968,63.932058)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.87783366px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 481.53079,442.83849 -0.0153,6.77692 c 9.55428,-0.16929 14.45853,-10.06218 12.50577,-19.31642 l -8.12484,3.61854 c 0.006,4.27743 -1.21305,6.01267 -4.23942,6.33245 z"
|
||||
id="path3916-5-2-7-68"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:transform-center-x="6.427012"
|
||||
inkscape:transform-center-y="17.713434" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.87783366px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 480.46352,428.30022 0.0153,-6.77692 c -9.55428,0.16929 -14.45853,10.06218 -12.50577,19.31642 l 8.12484,-3.61854 c -0.006,-4.27743 1.21305,-6.01267 4.23942,-6.33245 z"
|
||||
id="path3916-5-2-7-6-5"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccc"
|
||||
inkscape:transform-center-x="-6.4270161"
|
||||
inkscape:transform-center-y="-17.713434" />
|
||||
</g>
|
||||
<g
|
||||
transform="matrix(0.95191143,0,0,0.94999997,5.2301333,72.549709)"
|
||||
id="g4113">
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
||||
id="path3007-7-8"
|
||||
sodipodi:cx="425"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:ry="14.196428"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
transform="matrix(1.382716,0,0,1.4088051,-152.12422,-294.96682)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:1.23868203;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3007-4-8"
|
||||
sodipodi:cx="425"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:ry="14.196428"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
transform="matrix(0.57659584,0,0,0.58747505,190.47685,130.10497)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="path3007-0-2"
|
||||
sodipodi:cx="425"
|
||||
sodipodi:cy="517.54077"
|
||||
sodipodi:rx="14.464286"
|
||||
sodipodi:ry="14.196428"
|
||||
d="m 439.46429,517.54077 c 0,7.84047 -6.47589,14.19643 -14.46429,14.19643 -7.9884,0 -14.46429,-6.35596 -14.46429,-14.19643 0,-7.84047 6.47589,-14.19643 14.46429,-14.19643 7.9884,0 14.46429,6.35596 14.46429,14.19643 z"
|
||||
transform="matrix(0.27721059,0,0,0.282441,317.71558,287.97253)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.66094333;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(1.0590923,0,0,1.0590923,-66.309468,-40.31252)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.44283628;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-4"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(1.5807196,0,0,1.5807196,-313.47698,-273.99486)" />
|
||||
<path
|
||||
sodipodi:type="arc"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.53326446;stroke-linecap:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
|
||||
id="path3889-5"
|
||||
sodipodi:cx="473.83929"
|
||||
sodipodi:cy="447.98718"
|
||||
sodipodi:rx="10.982142"
|
||||
sodipodi:ry="10.982142"
|
||||
d="m 484.82144,447.98718 c 0,6.06527 -4.91688,10.98215 -10.98215,10.98215 -6.06527,0 -10.98214,-4.91688 -10.98214,-10.98215 0,-6.06527 4.91687,-10.98214 10.98214,-10.98214 6.06527,0 10.98215,4.91687 10.98215,10.98214 z"
|
||||
transform="matrix(1.3126696,0,0,1.3126696,-186.46436,-153.9119)" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.14413512px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 436.31917,443.49132 -0.10217,8.33905 c 12.45268,-0.22064 18.92699,-12.29179 16.38184,-24.35342 l -7.29812,4.63399 c 1.81826,5.98647 -4.04967,10.71674 -8.98155,11.38038 z"
|
||||
id="path3916-5"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:transform-center-x="8.3767009"
|
||||
inkscape:transform-center-y="23.087015" />
|
||||
<path
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.14413512px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 434.01553,424.88159 0.48098,-8.21278 c -12.45268,0.22064 -18.92699,12.29179 -16.38184,24.35342 l 8.05573,-3.62383 c -1.81826,-5.98647 2.91325,-11.85317 7.84513,-12.51681 z"
|
||||
id="path3916-5-7"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccc"
|
||||
inkscape:transform-center-x="-8.3766982"
|
||||
inkscape:transform-center-y="-23.087015" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 16 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.2 KiB |
|
@ -0,0 +1,112 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="100"
|
||||
height="100"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.2 r9819"
|
||||
sodipodi:docname="New document 1">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="5.6"
|
||||
inkscape:cx="56.013691"
|
||||
inkscape:cy="69.593676"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1003"
|
||||
inkscape:window-x="-9"
|
||||
inkscape:window-y="-9"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-277.5,-381.16879)">
|
||||
<g
|
||||
id="g3797"
|
||||
inkscape:export-filename="C:\Users\Jon\SkyDrive\Projects\Git\MvcMusicStore\MvcMusicStore\Content\Images\placeholder.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<rect
|
||||
y="381.16879"
|
||||
x="277.5"
|
||||
height="100"
|
||||
width="100"
|
||||
id="rect3017"
|
||||
style="fill:#ffffff;fill-opacity:1" />
|
||||
<g
|
||||
transform="translate(2.0535545,-3.2469826)"
|
||||
id="g3787">
|
||||
<rect
|
||||
style="fill:#edecb3;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.15819502px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
id="rect2998"
|
||||
width="78.591805"
|
||||
height="78.591805"
|
||||
x="286.15054"
|
||||
y="395.11987" />
|
||||
<g
|
||||
style="font-size:10.29434204px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#90905a;fill-opacity:1;stroke:none;font-family:Segoe UI;-inkscape-font-specification:Segoe UI"
|
||||
id="text3000">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 290.05246,405.88284 0,-0.99526 c 0.11393,0.10054 0.25048,0.19101 0.40966,0.27144 0.15917,0.0804 0.32672,0.14828 0.50265,0.20357 0.17593,0.0553 0.35269,0.098 0.5303,0.12818 0.1776,0.0302 0.3418,0.0452 0.4926,0.0452 0.51941,0 0.90729,-0.0963 1.16364,-0.28903 0.25635,-0.19268 0.38453,-0.46998 0.38453,-0.83189 0,-0.19436 -0.0427,-0.36358 -0.12817,-0.50768 -0.0855,-0.14409 -0.20358,-0.27562 -0.35437,-0.39458 -0.1508,-0.11896 -0.32925,-0.2329 -0.53533,-0.34181 -0.20609,-0.1089 -0.4281,-0.22367 -0.66602,-0.34431 -0.25132,-0.12734 -0.4859,-0.25635 -0.70371,-0.38705 -0.21782,-0.13068 -0.40715,-0.27478 -0.568,-0.43228 -0.16085,-0.15749 -0.28735,-0.33593 -0.3795,-0.53533 -0.0922,-0.19938 -0.13823,-0.43311 -0.13823,-0.7012 0,-0.32839 0.072,-0.61407 0.21614,-0.85702 0.14409,-0.24294 0.33342,-0.44317 0.568,-0.60067 0.23457,-0.15749 0.50181,-0.27478 0.80173,-0.35186 0.29991,-0.0771 0.60569,-0.1156 0.91734,-0.11561 0.71042,1e-5 1.22815,0.0855 1.5532,0.25635 l 0,0.95002 c -0.42558,-0.29488 -0.9718,-0.44233 -1.63865,-0.44234 -0.18431,10e-6 -0.36861,0.0193 -0.55292,0.0578 -0.18431,0.0385 -0.34851,0.10137 -0.4926,0.18849 -0.14409,0.0871 -0.26138,0.1994 -0.35186,0.33678 -0.0905,0.1374 -0.13571,0.30495 -0.13571,0.50266 0,0.18431 0.0343,0.34348 0.10304,0.47752 0.0687,0.13404 0.17006,0.25635 0.30411,0.36693 0.13404,0.11059 0.2974,0.21782 0.49008,0.3217 0.19269,0.10389 0.41469,0.21782 0.66602,0.34181 0.25803,0.12734 0.50265,0.26138 0.73387,0.40212 0.23122,0.14074 0.43396,0.29657 0.60821,0.46747 0.17425,0.1709 0.31248,0.36023 0.41469,0.56799 0.10221,0.20777 0.15331,0.44569 0.15331,0.71377 0,0.35521 -0.0695,0.65597 -0.2086,0.90226 -0.13907,0.24631 -0.32673,0.44653 -0.56297,0.60068 -0.23625,0.15414 -0.50852,0.26556 -0.81681,0.33426 -0.3083,0.0687 -0.63335,0.10304 -0.97515,0.10304 -0.11394,0 -0.25468,-0.009 -0.42223,-0.0276 -0.16755,-0.0184 -0.33845,-0.0452 -0.5127,-0.0804 -0.17426,-0.0352 -0.3393,-0.0788 -0.49512,-0.13069 -0.15582,-0.0519 -0.28065,-0.10974 -0.37447,-0.17341 z"
|
||||
id="path3005" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 299.43197,406.17438 -0.82435,0 0,-0.80425 -0.0201,0 c -0.35856,0.61659 -0.88635,0.92488 -1.58336,0.92488 -0.51271,0 -0.91399,-0.13571 -1.20385,-0.40715 -0.28987,-0.27143 -0.4348,-0.63166 -0.4348,-1.0807 0,-0.96174 0.56632,-1.52136 1.69897,-1.67886 l 1.54315,-0.21614 c -10e-6,-0.87462 -0.35354,-1.31192 -1.0606,-1.31193 -0.61994,10e-6 -1.17956,0.21112 -1.67886,0.63334 l 0,-0.84445 c 0.506,-0.3217 1.08908,-0.48255 1.74923,-0.48255 1.20971,0 1.81457,0.64005 1.81458,1.92014 z m -0.82435,-2.60375 -1.24156,0.1709 c -0.38202,0.0536 -0.6702,0.14829 -0.86456,0.284 -0.19436,0.13572 -0.29154,0.37616 -0.29154,0.72131 0,0.25133 0.0896,0.45658 0.26892,0.61575 0.17928,0.15918 0.41804,0.23876 0.71628,0.23876 0.40882,0 0.74644,-0.14325 1.01285,-0.42977 0.2664,-0.28651 0.3996,-0.64926 0.39961,-1.08824 z"
|
||||
id="path3007" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 308.3038,406.17438 -0.82435,0 0,-2.9556 c -10e-6,-0.56968 -0.088,-0.98185 -0.26389,-1.23653 -0.17594,-0.25468 -0.47167,-0.38201 -0.88719,-0.38202 -0.35186,10e-6 -0.65094,0.16085 -0.89723,0.48255 -0.24631,0.3217 -0.36946,0.70707 -0.36945,1.1561 l 0,2.9355 -0.82436,0 0,-3.05614 c 0,-1.012 -0.39039,-1.518 -1.17118,-1.51801 -0.36191,10e-6 -0.66015,0.15164 -0.89472,0.4549 -0.23457,0.30327 -0.35186,0.69786 -0.35186,1.18375 l 0,2.9355 -0.82435,0 0,-5.14717 0.82435,0 0,0.81429 0.0201,0 c 0.36526,-0.62328 0.89807,-0.93493 1.59844,-0.93493 0.35185,0 0.65847,0.098 0.91985,0.29405 0.26138,0.19604 0.44066,0.45323 0.53784,0.77157 0.38201,-0.71041 0.95169,-1.06562 1.70902,-1.06562 1.13264,0 1.69896,0.69869 1.69897,2.09606 z"
|
||||
id="path3009" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 310.70648,405.43045 -0.0201,0 0,3.11143 -0.82435,0 0,-7.51467 0.82435,0 0,0.90477 0.0201,0 c 0.40548,-0.6836 0.99861,-1.02541 1.7794,-1.02541 0.6635,0 1.18123,0.23039 1.5532,0.69115 0.37195,0.46077 0.55794,1.07819 0.55794,1.85228 0,0.86121 -0.20944,1.55068 -0.62831,2.06841 -0.41889,0.51774 -0.99191,0.7766 -1.71908,0.7766 -0.66686,0 -1.18124,-0.28818 -1.54315,-0.86456 z m -0.0201,-2.07596 0,0.7188 c 0,0.42558 0.13823,0.78665 0.41469,1.08321 0.27645,0.29657 0.62747,0.44485 1.05306,0.44485 0.4993,0 0.89053,-0.191 1.17369,-0.57302 0.28316,-0.38202 0.42474,-0.91315 0.42474,-1.59341 0,-0.57303 -0.13237,-1.02206 -0.39709,-1.34711 -0.26474,-0.32505 -0.6233,-0.48757 -1.07568,-0.48758 -0.4792,10e-6 -0.86457,0.16672 -1.1561,0.50014 -0.29154,0.33343 -0.43731,0.75147 -0.43731,1.25412 z"
|
||||
id="path3011" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 316.73833,406.17438 -0.82436,0 0,-7.62023 0.82436,0 z"
|
||||
id="path3013" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 322.54397,403.80688 -3.63418,0 c 0.0134,0.57303 0.16755,1.01536 0.46244,1.327 0.29489,0.31165 0.70036,0.46747 1.21642,0.46747 0.57972,0 1.11254,-0.191 1.59844,-0.57302 l 0,0.77408 c -0.45239,0.3284 -1.05055,0.4926 -1.79448,0.4926 -0.72717,0 -1.29852,-0.23373 -1.71404,-0.7012 -0.41553,-0.46746 -0.62329,-1.1251 -0.62329,-1.97291 0,-0.80089 0.22703,-1.4535 0.68109,-1.95784 0.45406,-0.50432 1.01787,-0.75649 1.69143,-0.75649 0.67355,0 1.19464,0.21782 1.56325,0.65345 0.36861,0.43564 0.55292,1.0405 0.55292,1.81458 z m -0.84446,-0.69869 c -0.003,-0.47584 -0.11812,-0.84613 -0.34431,-1.11086 -0.2262,-0.26473 -0.54036,-0.39709 -0.94248,-0.3971 -0.38872,10e-6 -0.7188,0.13907 -0.99023,0.4172 -0.27143,0.27814 -0.43898,0.64173 -0.50265,1.09076 z"
|
||||
id="path3015" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 8.4 KiB |
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.Admin.Catalog {
|
||||
angular.module("MusicStore.Admin.Catalog", []);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<div class="modal-header">
|
||||
<h3>Really delete this album?</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="text-center">
|
||||
<p>
|
||||
<img ng-src="{{ viewModel.album.AlbumArtUrl }}" ng-show="viewModel.album.AlbumArtUrl" alt="Album art" />
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ viewModel.album.Title }}</strong><br />
|
||||
<em>by {{ viewModel.album.Artist.Name }}</em>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<button type="submit" class="btn btn-danger" ng-click="viewModel.ok()">
|
||||
Delete Album
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" ng-click="viewModel.cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
@model MvcMusicStore.Models.Album
|
||||
|
||||
<div ng-controller="MusicStore.Admin.Catalog.AlbumDetailsController as viewModel">
|
||||
<h2>Album <small>Details</small></h2>
|
||||
<hr />
|
||||
|
||||
<form class="form-horizontal" role="form">
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static">{{ viewModel.album.Artist.Name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static">{{ viewModel.album.Genre.Name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Title, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static">{{ viewModel.album.Title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.Price, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static">{{ viewModel.album.Price | currency }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@Html.LabelFor(m => m.AlbumArtUrl, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static">{{ viewModel.album.AlbumArtUrl }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="viewModel.album.AlbumArtUrl">
|
||||
<label class="col-md-2 control-label">Album Art</label>
|
||||
<div class="col-md-10">
|
||||
<p class="form-control-static"><img ng-src="{{ viewModel.album.AlbumArtUrl }}" alt="Album Art" /></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<a class="btn btn-primary" ng-href="#/albums/{{ viewModel.album.AlbumId }}/edit">Edit</a>
|
||||
<button type="button" class="btn btn-danger" ng-click="viewModel.deleteAlbum()">Delete</button>
|
||||
<a class="btn btn-default" ng-href="#/albums">Back to List</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,70 @@
|
|||
/// <reference path="..\..\MusicStore.Admin.Catalog.ng.ts" />
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
@model MvcMusicStore.Models.Album
|
||||
|
||||
<div ng-controller="MusicStore.Admin.Catalog.AlbumEditController as viewModel">
|
||||
<h2>Album <small>{{ viewModel.mode | titlecase }}</small></h2>
|
||||
<hr />
|
||||
|
||||
<alert ng-show="viewModel.alert" type="viewModel.alert.type" close="viewModel.clearAlert()">
|
||||
{{ viewModel.alert.message }}
|
||||
<ul ng-show="viewModel.alert.modelErrors">
|
||||
<li ng-repeat="modelError in viewModel.alert.modelErrors">{{ modelError.ErrorMessage }}</li>
|
||||
</ul>
|
||||
</alert>
|
||||
|
||||
<form name="editAlbum" class="form-horizontal" ng-submit="viewModel.save()" novalidate>
|
||||
<div class="form-group" ng-class="@Html.ngValidationClassFor(m => m.ArtistId, formName: "editAlbum", className: "has-error")">
|
||||
@Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@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" })
|
||||
</div>
|
||||
</div>
|
||||
@Html.ngValidationMessageFor(m => m.ArtistId, "editAlbum", new { @class = "help-block field-validation-error" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="@Html.ngValidationClassFor(m => m.GenreId, formName: "editAlbum", className: "has-error")">
|
||||
@Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" })
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@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" })
|
||||
</div>
|
||||
</div>
|
||||
@Html.ngValidationMessageFor(m => m.GenreId, "editAlbum", new { @class = "help-block field-validation-error" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="@Html.ngValidationClassFor(m => m.Title, formName: "editAlbum", className: "has-error")">
|
||||
@Html.LabelFor(m => m.Title, new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@Html.ngTextBoxFor(m => m.Title, new { @class = "form-control", ng_model = "viewModel.album.Title", ng_disabled = "viewModel.disabled" })
|
||||
</div>
|
||||
</div>
|
||||
@Html.ngValidationMessageFor(model => model.Title, "editAlbum", new { @class = "help-block field-validation-error" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="@Html.ngValidationClassFor(m => m.Price, formName: "editAlbum", className: "has-error")">
|
||||
@Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">$</span>
|
||||
@Html.ngTextBoxFor(m => m.Price, new { @class = "form-control", ng_model = "viewModel.album.Price", ng_disabled = "viewModel.disabled" })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@Html.ngValidationMessageFor(model => model.Price, "editAlbum", new { @class = "help-block field-validation-error" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="@Html.ngValidationClassFor(m => m.AlbumArtUrl, formName: "editAlbum", className: "has-error")">
|
||||
@Html.LabelFor(m => m.AlbumArtUrl, new { @class = "control-label col-md-2" })
|
||||
<div class="col-md-10">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
@Html.ngTextBoxFor(m => m.AlbumArtUrl, new { @class = "form-control", ng_model = "viewModel.album.AlbumArtUrl", ng_disabled = "viewModel.disabled" })
|
||||
</div>
|
||||
</div>
|
||||
@Html.ngValidationMessageFor(model => model.AlbumArtUrl, "editAlbum", new { @class = "field-validation-error" })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-show="viewModel.album.AlbumArtUrl && !editAlbum.AlbumArtUrl.$invalid">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<img ng-src="{{ viewModel.album.AlbumArtUrl }}" alt="Album Art" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-2 col-md-10">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="viewModel.disabled || !viewModel.album || editAlbum.$invalid">
|
||||
Save
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger"
|
||||
ng-click="viewModel.deleteAlbum()"
|
||||
ng-show="viewModel.mode === 'edit'">Delete</button>
|
||||
<a class="btn btn-default" ng-href="#/albums">Back to List</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,205 @@
|
|||
/// <reference path="..\..\MusicStore.Admin.Catalog.ng.ts" />
|
||||
|
||||
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<Models.IArtist>;
|
||||
genres: Array<Models.IGenreLookup>;
|
||||
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<Models.IArtist>;
|
||||
|
||||
public genres: Array<Models.IGenreLookup>;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -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<Models.IArtist>;
|
||||
genres: Array<Models.IGenreLookup>;
|
||||
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<Models.IArtist>;
|
||||
|
||||
public genres: Array<Models.IGenreLookup>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
@model MvcMusicStore.Models.Album
|
||||
|
||||
<div ng-controller="MusicStore.Admin.Catalog.AlbumListController as viewModel">
|
||||
<h2>Albums</h2>
|
||||
<p>
|
||||
<a class="btn btn-default" href="#/albums/new">Create new</a>
|
||||
</p>
|
||||
|
||||
<alert ng-show="viewModel.alert" type="viewModel.alert.type" close="viewModel.clearAlert()">
|
||||
{{ viewModel.alert.message }}
|
||||
<ul ng-show="viewModel.alert.modelErrors">
|
||||
<li ng-repeat="modelError in viewModel.alert.modelErrors">{{ modelError.ErrorMessage }}</li>
|
||||
</ul>
|
||||
</alert>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a title="Sort by this column" href="#/albums" ng-click="viewModel.sortBy('@Html.NameFor(m => m.Genre.Name)')">@Html.DisplayNameFor(m => m.Genre)</a>
|
||||
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet" title="Sorted ascending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Genre.Name)' && !viewModel.sortDescending"></span>
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet-alt" title="Sorted descending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Genre.Name)' && viewModel.sortDescending"></span>
|
||||
</th>
|
||||
<th>
|
||||
<a title="Sort by this column" href="#/albums" ng-click="viewModel.sortBy('@Html.NameFor(m => m.Artist.Name)')">@Html.DisplayNameFor(m => m.Artist)</a>
|
||||
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet" title="Sorted ascending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Artist.Name)' && !viewModel.sortDescending"></span>
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet-alt" title="Sorted descending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Artist.Name)' && viewModel.sortDescending"></span>
|
||||
</th>
|
||||
<th>
|
||||
<a title="Sort by this column" href="#/albums" ng-click="viewModel.sortBy('@Html.NameFor(m => m.Title)')">@Html.DisplayNameFor(m => m.Title)</a>
|
||||
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet" title="Sorted ascending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Title)' && !viewModel.sortDescending"></span>
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet-alt" title="Sorted descending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Title)' && viewModel.sortDescending"></span>
|
||||
</th>
|
||||
<th>
|
||||
<a title="Sort by this column" href="#/albums" ng-click="viewModel.sortBy('@Html.NameFor(m => m.Price)')">@Html.DisplayNameFor(m => m.Price)</a>
|
||||
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet" title="Sorted ascending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Price)' && !viewModel.sortDescending"></span>
|
||||
<span class="glyphicon glyphicon-sort-by-alphabet-alt" title="Sorted descending" ng-show="viewModel.sortColumn == '@Html.NameFor(m => m.Price)' && viewModel.sortDescending"></span>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="album in viewModel.albums">
|
||||
<td>
|
||||
{{ album.Genre.Name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ album.Artist.Name | truncate:25 }}
|
||||
</td>
|
||||
<td>
|
||||
{{ album.Title | truncate:25 }}
|
||||
</td>
|
||||
<td>
|
||||
{{ album.Price | currency }}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-xs">
|
||||
<a ng-href="#/albums/{{ album.AlbumId }}/details" class="btn btn-default">Details</a>
|
||||
<a ng-href="#/albums/{{ album.AlbumId }}/edit" class="btn btn-default">Edit</a>
|
||||
<a ng-click="viewModel.deleteAlbum(album)" class="btn btn-default">Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<pagination class="pagination-sm" on-select-page="viewModel.loadPage(page)"
|
||||
page="viewModel.currentPage" items-per-page="viewModel.pageSize"
|
||||
total-items="viewModel.totalCount" max-size="6"
|
||||
rotate="false" boundary-links="true"></pagination>
|
||||
<p>
|
||||
{{ viewModel.totalCount }} total albums
|
||||
</p>
|
||||
</div>
|
|
@ -0,0 +1,135 @@
|
|||
/// <reference path="..\..\MusicStore.Admin.Catalog.ng.ts" />
|
||||
|
||||
module MusicStore.Admin.Catalog {
|
||||
interface IAlbumListViewModel {
|
||||
albums: Array<Models.IAlbum>;
|
||||
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<Models.IAlbum>;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
module MusicStore.Admin.Catalog {
|
||||
interface IAlbumListViewModel {
|
||||
albums: Array<Models.IAlbum>;
|
||||
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<Models.IAlbum>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/// <reference path="../references.ts" />
|
||||
|
||||
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() {
|
||||
|
||||
//}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/// <reference path="../references.ts" />
|
||||
|
||||
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() {
|
||||
|
||||
//}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.AlbumApi {
|
||||
angular.module("MusicStore.AlbumApi", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.ArtistApi {
|
||||
angular.module("MusicStore.ArtistApi", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.GenreApi {
|
||||
angular.module("MusicStore.GenreApi", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.GenreMenu {
|
||||
angular.module("MusicStore.GenreMenu", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.InlineData {
|
||||
angular.module("MusicStore.InlineData", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.LoginLink {
|
||||
angular.module("MusicStore.LoginLink", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.PreventSubmit {
|
||||
angular.module("MusicStore.PreventSubmit", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.Store.Catalog {
|
||||
angular.module("MusicStore.Store.Catalog", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.Store.Home {
|
||||
angular.module("MusicStore.Store.Home", []);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<div ng-controller="MusicStore.Store.Catalog.AlbumDetailsController as viewModel">
|
||||
<h2>{{ viewModel.album.Title }}</h2>
|
||||
|
||||
<p>
|
||||
<img ng-alt="{{ viewModel.album.Title }}" ng-src="{{ viewModel.album.AlbumArtUrl}}" />
|
||||
</p>
|
||||
|
||||
<div id="album-details">
|
||||
<p>
|
||||
<em>Genre:</em>
|
||||
{{ viewModel.album.Genre.Name }}
|
||||
</p>
|
||||
<p>
|
||||
<em>Artist:</em>
|
||||
{{ viewModel.album.Artist.Name }}
|
||||
</p>
|
||||
<p>
|
||||
<em>Price:</em>
|
||||
{{ viewModel.album.Price | currency }}
|
||||
</p>
|
||||
<p class="button">
|
||||
<!-- TODO: Shopping cart functionality -->
|
||||
Add to cart
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,31 @@
|
|||
/// <reference path="..\..\MusicStore.Store.Catalog.ng.ts" />
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<div ng-controller="MusicStore.Store.Catalog.GenreDetailsController as viewModel" class="genre">
|
||||
<h3><em>{{ viewModel.genre.Name }}</em> Albums</h3>
|
||||
|
||||
<ul id="album-list" class="list-unstyled">
|
||||
<li ng-repeat="album in viewModel.albums" class="col-lg-2 col-md-2 col-sm-2 col-xs-4 container">
|
||||
<a ng-href="#/albums/{{ album.AlbumId }}">
|
||||
<img ng-show="album.AlbumArtUrl" ng-alt="{{ album.Title }}" ng-src="{{ album.AlbumArtUrl }}" />
|
||||
<h5 class="control-label">{{ album.Title }}</h5>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
/// <reference path="..\..\MusicStore.Store.Catalog.ng.ts" />
|
||||
|
||||
module MusicStore.Store.Catalog {
|
||||
interface IGenreDetailsViewModel {
|
||||
albums: Array<Models.IAlbum>;
|
||||
}
|
||||
|
||||
interface IGenreDetailsRouteParams extends ng.route.IRouteParamsService {
|
||||
genreId: number;
|
||||
}
|
||||
|
||||
class GenreDetailsController implements IGenreDetailsViewModel {
|
||||
public albums: Array<Models.IAlbum>;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
module MusicStore.Store.Catalog {
|
||||
interface IGenreDetailsViewModel {
|
||||
albums: Array<Models.IAlbum>;
|
||||
}
|
||||
|
||||
interface IGenreDetailsRouteParams extends ng.route.IRouteParamsService {
|
||||
genreId: number;
|
||||
}
|
||||
|
||||
class GenreDetailsController implements IGenreDetailsViewModel {
|
||||
public albums: Array<Models.IAlbum>;
|
||||
|
||||
constructor($routeParams: IGenreDetailsRouteParams, genreApi: GenreApi.IGenreApiService) {
|
||||
var viewModel = this;
|
||||
|
||||
genreApi.getGenreAlbums($routeParams.genreId).success(result => {
|
||||
viewModel.albums = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<div ng-controller="MusicStore.Store.Catalog.GenreListController as viewModel">
|
||||
<h3>Browse Genres</h3>
|
||||
|
||||
<p>
|
||||
Select from {{ viewModel.genres.length }} genres:
|
||||
</p>
|
||||
<ul class="list-group">
|
||||
<li ng-repeat="genre in viewModel.genres" class="list-group-item">
|
||||
<a ng-href="#/albums/genres/{{ genre.GenreId }}">{{ genre.Name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
/// <reference path="..\..\MusicStore.Store.Catalog.ng.ts" />
|
||||
|
||||
module MusicStore.Store.Catalog {
|
||||
interface IGenreListViewModel {
|
||||
genres: Array<Models.IGenre>;
|
||||
}
|
||||
|
||||
class GenreListController implements IGenreListViewModel {
|
||||
public genres: Array<Models.IGenre>;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
module MusicStore.Store.Catalog {
|
||||
interface IGenreListViewModel {
|
||||
genres: Array<Models.IGenre>;
|
||||
}
|
||||
|
||||
class GenreListController implements IGenreListViewModel {
|
||||
public genres: Array<Models.IGenre>;
|
||||
|
||||
constructor(genreApi: GenreApi.IGenreApiService) {
|
||||
var viewModel = this;
|
||||
|
||||
genreApi.getGenresList().success(function (genres) {
|
||||
viewModel.genres = genres;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<div class="jumbotron">
|
||||
<h1>MVC Music Store</h1>
|
||||
<img src="Images/home-showcase.png" />
|
||||
</div>
|
||||
|
||||
<div ng-controller="MusicStore.Store.Home.HomeController as viewModel">
|
||||
<ul class="row list-unstyled" id="album-list">
|
||||
<li ng-repeat="album in viewModel.albums" class="col-lg-2 col-md-2 col-sm-2 col-xs-4 container">
|
||||
<a ng-href="#/albums/{{ album.AlbumId }}">
|
||||
<img ng-alt="{{ album.Title }}" ng-src="{{ album.AlbumArtUrl }}" />
|
||||
<h4>{{ album.Title }}</h4>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
/// <reference path="..\..\MusicStore.Store.Home.ng.ts" />
|
||||
|
||||
module MusicStore.Store.Home {
|
||||
interface IHomeViewModel {
|
||||
albums: Array<Models.IAlbum>
|
||||
}
|
||||
|
||||
class HomeController implements IHomeViewModel {
|
||||
public albums: Array<Models.IAlbum>;
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
module MusicStore.Store.Home {
|
||||
interface IHomeViewModel {
|
||||
albums: Array<Models.IAlbum>
|
||||
}
|
||||
|
||||
class HomeController implements IHomeViewModel {
|
||||
public albums: Array<Models.IAlbum>;
|
||||
|
||||
constructor(albumApi: AlbumApi.IAlbumApiService) {
|
||||
var viewModel = this;
|
||||
|
||||
albumApi.getMostPopularAlbums().then(albums => {
|
||||
viewModel.albums = albums;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/// <reference path="../references.ts" />
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/// <reference path="../references.ts" />
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.TitleCase {
|
||||
angular.module("MusicStore.TitleCase", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.Truncate {
|
||||
angular.module("MusicStore.Truncate", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.UrlResolver {
|
||||
angular.module("MusicStore.UrlResolver", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.UserDetails {
|
||||
angular.module("MusicStore.UserDetails", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.ViewAlert {
|
||||
angular.module("MusicStore.ViewAlert", []);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module MusicStore.Visited {
|
||||
angular.module("MusicStore.Visited", []);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
|
||||
<pages pageBaseType="System.Web.Mvc.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="MvcMusicStore" />
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
|
||||
<system.webServer>
|
||||
<handlers>
|
||||
<remove name="BlockViewHandler"/>
|
||||
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
|
@ -0,0 +1,109 @@
|
|||
/// <reference path="..\..\MusicStore.AlbumApi.ng.ts" />
|
||||
|
||||
module MusicStore.AlbumApi {
|
||||
export interface IAlbumApiService {
|
||||
getAlbums(page?: number, pageSize?: number, sortBy?: string): ng.IPromise<Models.IPagedList<Models.IAlbum>>;
|
||||
getAlbumDetails(albumId: number): ng.IPromise<Models.IAlbum>;
|
||||
getMostPopularAlbums(count?: number): ng.IPromise<Array<Models.IAlbum>>;
|
||||
createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
deleteAlbum(albumId: number, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
module MusicStore.AlbumApi {
|
||||
export interface IAlbumApiService {
|
||||
getAlbums(page?: number, pageSize?: number, sortBy?: string): ng.IPromise<Models.IPagedList<Models.IAlbum>>;
|
||||
getAlbumDetails(albumId: number): ng.IPromise<Models.IAlbum>;
|
||||
getMostPopularAlbums(count?: number): ng.IPromise<Array<Models.IAlbum>>;
|
||||
createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
deleteAlbum(albumId: number, config?: ng.IRequestConfig): ng.IHttpPromise<Models.IApiResult>;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/// <reference path="..\..\MusicStore.ArtistApi.ng.ts" />
|
||||
|
||||
module MusicStore.ArtistApi {
|
||||
export interface IArtistApiService {
|
||||
getArtistsLookup(): ng.IPromise<Array<Models.IArtist>>;
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
module MusicStore.ArtistApi {
|
||||
export interface IArtistApiService {
|
||||
getArtistsLookup(): ng.IPromise<Array<Models.IArtist>>;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/// <reference path="..\..\MusicStore.GenreApi.ng.ts" />
|
||||
|
||||
module MusicStore.GenreApi {
|
||||
export interface IGenreApiService {
|
||||
getGenresLookup(): ng.IPromise<Array<Models.IGenreLookup>>;
|
||||
getGenresMenu(): ng.IPromise<Array<Models.IGenre>>;
|
||||
getGenresList(): ng.IHttpPromise<Array<Models.IGenre>>;
|
||||
getGenreAlbums(genreId: number): ng.IHttpPromise<Array<Models.IAlbum>>;
|
||||
}
|
||||
|
||||
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
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
module MusicStore.GenreApi {
|
||||
export interface IGenreApiService {
|
||||
getGenresLookup(): ng.IPromise<Array<Models.IGenreLookup>>;
|
||||
getGenresMenu(): ng.IPromise<Array<Models.IGenre>>;
|
||||
getGenresList(): ng.IHttpPromise<Array<Models.IGenre>>;
|
||||
getGenreAlbums(genreId: number): ng.IHttpPromise<Array<Models.IAlbum>>;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<li class="dropdown" ng-controller="MusicStore.GenreMenu.GenreMenuController as viewModel">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown">Store <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="genre in viewModel.genres">
|
||||
<a ng-href="{{ viewModel.urlBase }}#/albums/genres/{{ genre.GenreId }}" title="{{ genre.Description }}">{{ genre.Name }}</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href="{{ viewModel.urlBase }}#/albums/genres">More…</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
|
@ -0,0 +1,31 @@
|
|||
/// <reference path="..\..\MusicStore.GenreMenu.ng.ts" />
|
||||
|
||||
module MusicStore.GenreMenu {
|
||||
interface IGenreMenuViewModel {
|
||||
genres: Array<Models.IGenre>;
|
||||
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<Models.IGenre>;
|
||||
|
||||
public urlBase: string;
|
||||
}
|
||||
|
||||
angular.module("MusicStore.GenreMenu")
|
||||
.controller("MusicStore.GenreMenu.GenreMenuController", [
|
||||
"MusicStore.GenreApi.IGenreApiService",
|
||||
"MusicStore.UrlResolver.IUrlResolverService",
|
||||
GenreMenuController
|
||||
]);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
module MusicStore.GenreMenu {
|
||||
interface IGenreMenuViewModel {
|
||||
genres: Array<Models.IGenre>;
|
||||
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<Models.IGenre>;
|
||||
|
||||
public urlBase: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/// <reference path="..\..\MusicStore.GenreMenu.ng.ts" />
|
||||
|
||||
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);
|
||||
}
|
||||
]);
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/// <reference path="..\..\MusicStore.InlineData.ng.ts" />
|
||||
|
||||
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);
|
||||
}
|
||||
]);
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/// <reference path="..\..\MusicStore.LoginLink.ng.ts" />
|
||||
|
||||
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);
|
||||
}
|
||||
]);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
module MusicStore.Models {
|
||||
export interface IAlert {
|
||||
type: AlertType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IModelErrorAlert extends IAlert {
|
||||
modelErrors: Array<IModelError>;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
module MusicStore.Models {
|
||||
export interface IAlert {
|
||||
type: AlertType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface IModelErrorAlert extends IAlert {
|
||||
modelErrors: Array<IModelError>;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module MusicStore.Models {
|
||||
export interface IApiResult {
|
||||
Message?: string;
|
||||
Data?: any;
|
||||
ModelErrors?: Array<IModelError>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module MusicStore.Models {
|
||||
export interface IApiResult {
|
||||
Message?: string;
|
||||
Data?: any;
|
||||
ModelErrors?: Array<IModelError>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IArtist {
|
||||
ArtistId: number;
|
||||
Name: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IArtist {
|
||||
ArtistId: number;
|
||||
Name: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module MusicStore.Models {
|
||||
export interface IGenre {
|
||||
GenreId: number;
|
||||
Name: string;
|
||||
Description: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
module MusicStore.Models {
|
||||
export interface IGenre {
|
||||
GenreId: number;
|
||||
Name: string;
|
||||
Description: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IGenreLookup {
|
||||
GenreId: number;
|
||||
Name: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IGenreLookup {
|
||||
GenreId: number;
|
||||
Name: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IModelError {
|
||||
FieldName: string;
|
||||
ErrorMessage: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module MusicStore.Models {
|
||||
export interface IModelError {
|
||||
FieldName: string;
|
||||
ErrorMessage: string;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module MusicStore.Models {
|
||||
export interface IPagedList<T> {
|
||||
Data: Array<T>;
|
||||
Page: number;
|
||||
PageSize: number;
|
||||
TotalCount: number;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module MusicStore.Models {
|
||||
export interface IPagedList<T> {
|
||||
Data: Array<T>;
|
||||
Page: number;
|
||||
PageSize: number;
|
||||
TotalCount: number;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module MusicStore.Models {
|
||||
export interface IUserDetails {
|
||||
isAuthenticated: boolean;
|
||||
userName: string;
|
||||
userId: string;
|
||||
roles: Array<string>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
module MusicStore.Models {
|
||||
export interface IUserDetails {
|
||||
isAuthenticated: boolean;
|
||||
userName: string;
|
||||
userId: string;
|
||||
roles: Array<string>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/// <reference path="..\..\MusicStore.PreventSubmit.ng.ts" />
|
||||
|
||||
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 <form> 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();
|
||||
}
|
||||
]);
|
||||
}
|
|
@ -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 <form> tags and no-op if no action attr
|
||||
|
||||
element.submit(e => {
|
||||
if (scope.$eval(attrs.appPreventSubmit)) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/// <reference path="..\..\MusicStore.TitleCase.ng.ts" />
|
||||
|
||||
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);
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче