Added new SPA port of original Music Store app

This commit is contained in:
DamianEdwards 2014-05-07 16:37:23 -07:00
Родитель 4c026726f6
Коммит d4efedeb17
174 изменённых файлов: 11669 добавлений и 12 удалений

6
.gitignore поставляемый
Просмотреть файл

@ -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;
}

Двоичные данные
src/MvcMusicStore.Spa/Client/favicon.ico Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 31 KiB

Двоичные данные
src/MvcMusicStore.Spa/Client/images/home-showcase.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 248 KiB

Двоичные данные
src/MvcMusicStore.Spa/Client/images/logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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

Двоичные данные
src/MvcMusicStore.Spa/Client/images/placeholder.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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);
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше