Initial Upgrade to .NET Core 2.0 (#50)

* Ardalis/upgrade1 (#44)

* Upgrading to netcore 2.0
Updating repository to support async options and refactoring to use it.

* Starting work on tracking customer orders feature.

* Cleaning up some bugs
Working on basket view component implementation

* Fixing up styles, especially for basket in header.

* Adding Order Features (#47)

* Working on order model binding from checkout page - WIP

* Small layout tweaks (#43)

* Updating quantities implemented.

* Fixed basket widget count

* Order History (#49)

* working on creating and viewing orders.
* Working on wiring up listing of orders
* List orders page works as expected. Needed to support ThenInclude scenarios. Currently using strings.
This commit is contained in:
Steve Smith
2017-09-22 11:28:55 -04:00
committed by GitHub
parent b90bd08d11
commit aca618316a
70 changed files with 1755 additions and 513 deletions

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.4</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>

View File

@@ -0,0 +1,26 @@
using ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace ApplicationCore.Entities.BuyerAggregate
{
public class Buyer : BaseEntity, IAggregateRoot
{
public string IdentityGuid { get; private set; }
private List<PaymentMethod> _paymentMethods = new List<PaymentMethod>();
public IEnumerable<PaymentMethod> PaymentMethods => _paymentMethods.AsReadOnly();
protected Buyer()
{
}
public Buyer(string identity) : this()
{
IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity));
}
}
}

View File

@@ -0,0 +1,11 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
namespace ApplicationCore.Entities.BuyerAggregate
{
public class PaymentMethod : BaseEntity
{
public string Alias { get; set; }
public string CardId { get; set; } // actual card data must be stored in a PCI compliant system, like Stripe
public string Last4 { get; set; }
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
namespace ApplicationCore.Entities.OrderAggregate
{
public class Address // ValueObject
{
public String Street { get; private set; }
public String City { get; private set; }
public String State { get; private set; }
public String Country { get; private set; }
public String ZipCode { get; private set; }
private Address() { }
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
//protected override IEnumerable<object> GetAtomicValues()
//{
// yield return Street;
// yield return City;
// yield return State;
// yield return Country;
// yield return ZipCode;
//}
}
}

View File

@@ -0,0 +1,23 @@
namespace ApplicationCore.Entities.OrderAggregate
{
/// <summary>
/// Represents the item that was ordered. If catalog item details change, details of
/// the item that was part of a completed order should not change.
/// </summary>
public class CatalogItemOrdered // ValueObject
{
public CatalogItemOrdered(int catalogItemId, string productName, string pictureUri)
{
CatalogItemId = catalogItemId;
ProductName = productName;
PictureUri = pictureUri;
}
private CatalogItemOrdered()
{
// required by EF
}
public int CatalogItemId { get; private set; }
public string ProductName { get; private set; }
public string PictureUri { get; private set; }
}
}

View File

@@ -0,0 +1,48 @@
using ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Collections.Generic;
namespace ApplicationCore.Entities.OrderAggregate
{
public class Order : BaseEntity, IAggregateRoot
{
private Order()
{
}
public Order(string buyerId, Address shipToAddress, List<OrderItem> items)
{
ShipToAddress = shipToAddress;
_orderItems = items;
BuyerId = buyerId;
}
public string BuyerId { get; private set; }
public DateTimeOffset OrderDate { get; private set; } = DateTimeOffset.Now;
public Address ShipToAddress { get; private set; }
// DDD Patterns comment
// Using a private collection field, better for DDD Aggregate's encapsulation
// so OrderItems cannot be added from "outside the AggregateRoot" directly to the collection,
// but only through the method Order.AddOrderItem() which includes behavior.
private readonly List<OrderItem> _orderItems = new List<OrderItem>();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
// Using List<>.AsReadOnly()
// This will create a read only wrapper around the private list so is protected against "external updates".
// It's much cheaper than .ToList() because it will not have to copy all items in a new collection. (Just one heap alloc for the wrapper instance)
//https://msdn.microsoft.com/en-us/library/e78dcd75(v=vs.110).aspx
public decimal Total()
{
var total = 0m;
foreach (var item in _orderItems)
{
total += item.UnitPrice * item.Units;
}
return total;
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
namespace ApplicationCore.Entities.OrderAggregate
{
public class OrderItem : BaseEntity
{
public CatalogItemOrdered ItemOrdered { get; private set; }
public decimal UnitPrice { get; private set; }
public int Units { get; private set; }
protected OrderItem()
{
}
public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units)
{
ItemOrdered = itemOrdered;
UnitPrice = unitPrice;
Units = units;
}
}
}

View File

@@ -0,0 +1,5 @@
namespace ApplicationCore.Interfaces
{
public interface IAggregateRoot
{ }
}

View File

@@ -6,6 +6,7 @@
/// <typeparam name="T"></typeparam>
public interface IAppLogger<T>
{
void LogInformation(string message, params object[] args);
void LogWarning(string message, params object[] args);
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IAsyncRepository<T> where T : BaseEntity
{
Task<T> GetByIdAsync(int id);
Task<List<T>> ListAllAsync();
Task<List<T>> ListAsync(ISpecification<T> spec);
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
}
}

View File

@@ -1,7 +0,0 @@
namespace ApplicationCore.Interfaces
{
public interface IImageService
{
byte[] GetImageBytesById(int id);
}
}

View File

@@ -0,0 +1,12 @@
using ApplicationCore.Entities.OrderAggregate;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IOrderRepository : IRepository<Order>, IAsyncRepository<Order>
{
Order GetByIdWithItems(int id);
Task<Order> GetByIdWithItemsAsync(int id);
}
}

View File

@@ -0,0 +1,10 @@
using ApplicationCore.Entities.OrderAggregate;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IOrderService
{
Task CreateOrderAsync(int basketId, Address shippingAddress);
}
}

View File

@@ -1,16 +1,14 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
{
public interface IRepository<T> where T : BaseEntity
{
T GetById(int id);
List<T> List();
List<T> List(ISpecification<T> spec);
IEnumerable<T> ListAll();
IEnumerable<T> List(ISpecification<T> spec);
T Add(T entity);
void Update(T entity);
void Delete(T entity);

View File

@@ -8,6 +8,7 @@ namespace ApplicationCore.Interfaces
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
List<string> IncludeStrings { get; }
void AddInclude(Expression<Func<T, object>> includeExpression);
}
}

View File

@@ -1,9 +1,5 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
namespace ApplicationCore.Interfaces
namespace ApplicationCore.Interfaces
{
public interface IUriComposer
{
string ComposePicUri(string uriTemplate);

View File

@@ -3,6 +3,7 @@ using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Linq.Expressions;
using System.Collections.Generic;
using ApplicationCore.Entities.OrderAggregate;
namespace ApplicationCore.Specifications
{
@@ -28,6 +29,8 @@ namespace ApplicationCore.Specifications
public List<Expression<Func<Basket, object>>> Includes { get; } = new List<Expression<Func<Basket, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
public void AddInclude(Expression<Func<Basket, object>> includeExpression)
{
Includes.Add(includeExpression);

View File

@@ -24,6 +24,8 @@ namespace ApplicationCore.Specifications
public List<Expression<Func<CatalogItem, object>>> Includes { get; } = new List<Expression<Func<CatalogItem, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
public void AddInclude(Expression<Func<CatalogItem, object>> includeExpression)
{
Includes.Add(includeExpression);

View File

@@ -0,0 +1,35 @@
using ApplicationCore.Interfaces;
using System;
using System.Linq.Expressions;
using System.Collections.Generic;
using ApplicationCore.Entities.OrderAggregate;
namespace ApplicationCore.Specifications
{
public class CustomerOrdersWithItemsSpecification : ISpecification<Order>
{
private readonly string _buyerId;
public CustomerOrdersWithItemsSpecification(string buyerId)
{
_buyerId = buyerId;
AddInclude(o => o.OrderItems);
AddInclude("OrderItems.ItemOrdered");
}
public Expression<Func<Order, bool>> Criteria => o => o.BuyerId == _buyerId;
public List<Expression<Func<Order, object>>> Includes { get; } = new List<Expression<Func<Order, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
public void AddInclude(Expression<Func<Order, object>> includeExpression)
{
Includes.Add(includeExpression);
}
public void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.EntityFrameworkCore.Metadata;
using ApplicationCore.Entities.OrderAggregate;
namespace Infrastructure.Data
{
@@ -20,6 +21,8 @@ namespace Infrastructure.Data
public DbSet<CatalogItem> CatalogItems { get; set; }
public DbSet<CatalogBrand> CatalogBrands { get; set; }
public DbSet<CatalogType> CatalogTypes { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
@@ -27,6 +30,8 @@ namespace Infrastructure.Data
builder.Entity<CatalogBrand>(ConfigureCatalogBrand);
builder.Entity<CatalogType>(ConfigureCatalogType);
builder.Entity<CatalogItem>(ConfigureCatalogItem);
builder.Entity<Order>(ConfigureOrder);
builder.Entity<OrderItem>(ConfigureOrderItem);
}
private void ConfigureBasket(EntityTypeBuilder<Basket> builder)
@@ -36,7 +41,7 @@ namespace Infrastructure.Data
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
}
void ConfigureCatalogItem(EntityTypeBuilder<CatalogItem> builder)
private void ConfigureCatalogItem(EntityTypeBuilder<CatalogItem> builder)
{
builder.ToTable("Catalog");
@@ -63,7 +68,7 @@ namespace Infrastructure.Data
.HasForeignKey(ci => ci.CatalogTypeId);
}
void ConfigureCatalogBrand(EntityTypeBuilder<CatalogBrand> builder)
private void ConfigureCatalogBrand(EntityTypeBuilder<CatalogBrand> builder)
{
builder.ToTable("CatalogBrand");
@@ -78,7 +83,7 @@ namespace Infrastructure.Data
.HasMaxLength(100);
}
void ConfigureCatalogType(EntityTypeBuilder<CatalogType> builder)
private void ConfigureCatalogType(EntityTypeBuilder<CatalogType> builder)
{
builder.ToTable("CatalogType");
@@ -92,5 +97,14 @@ namespace Infrastructure.Data
.IsRequired()
.HasMaxLength(100);
}
private void ConfigureOrder(EntityTypeBuilder<Order> builder)
{
builder.OwnsOne(o => o.ShipToAddress);
}
private void ConfigureOrderItem(EntityTypeBuilder<OrderItem> builder)
{
builder.OwnsOne(i => i.ItemOrdered);
}
}
}

View File

@@ -10,39 +10,38 @@ namespace Infrastructure.Data
{
public class CatalogContextSeed
{
public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILoggerFactory loggerFactory, int? retry = 0)
public static async Task SeedAsync(IApplicationBuilder applicationBuilder,
CatalogContext catalogContext,
ILoggerFactory loggerFactory, int? retry = 0)
{
int retryForAvailability = retry.Value;
try
{
var context = (CatalogContext)applicationBuilder
.ApplicationServices.GetService(typeof(CatalogContext));
// TODO: Only run this if using a real database
// context.Database.Migrate();
if (!context.CatalogBrands.Any())
if (!catalogContext.CatalogBrands.Any())
{
context.CatalogBrands.AddRange(
catalogContext.CatalogBrands.AddRange(
GetPreconfiguredCatalogBrands());
await context.SaveChangesAsync();
await catalogContext.SaveChangesAsync();
}
if (!context.CatalogTypes.Any())
if (!catalogContext.CatalogTypes.Any())
{
context.CatalogTypes.AddRange(
catalogContext.CatalogTypes.AddRange(
GetPreconfiguredCatalogTypes());
await context.SaveChangesAsync();
await catalogContext.SaveChangesAsync();
}
if (!context.CatalogItems.Any())
if (!catalogContext.CatalogItems.Any())
{
context.CatalogItems.AddRange(
catalogContext.CatalogItems.AddRange(
GetPreconfiguredItems());
await context.SaveChangesAsync();
await catalogContext.SaveChangesAsync();
}
}
catch (Exception ex)
@@ -50,9 +49,9 @@ namespace Infrastructure.Data
if (retryForAvailability < 10)
{
retryForAvailability++;
var log = loggerFactory.CreateLogger("catalog seed");
var log = loggerFactory.CreateLogger<CatalogContextSeed>();
log.LogError(ex.Message);
await SeedAsync(applicationBuilder, loggerFactory, retryForAvailability);
await SeedAsync(applicationBuilder, catalogContext, loggerFactory, retryForAvailability);
}
}
}

View File

@@ -3,36 +3,68 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Infrastructure.Data
{
public class EfRepository<T> : IRepository<T> where T : BaseEntity
/// <summary>
/// "There's some repetition here - couldn't we have some the sync methods call the async?"
/// https://blogs.msdn.microsoft.com/pfxteam/2012/04/13/should-i-expose-synchronous-wrappers-for-asynchronous-methods/
/// </summary>
/// <typeparam name="T"></typeparam>
public class EfRepository<T> : IRepository<T>, IAsyncRepository<T> where T : BaseEntity
{
private readonly CatalogContext _dbContext;
protected readonly CatalogContext _dbContext;
public EfRepository(CatalogContext dbContext)
{
_dbContext = dbContext;
}
public T GetById(int id)
public virtual T GetById(int id)
{
return _dbContext.Set<T>().SingleOrDefault(e => e.Id == id);
return _dbContext.Set<T>().Find(id);
}
public List<T> List()
public virtual async Task<T> GetByIdAsync(int id)
{
return _dbContext.Set<T>().ToList();
return await _dbContext.Set<T>().FindAsync(id);
}
public List<T> List(ISpecification<T> spec)
public IEnumerable<T> ListAll()
{
return _dbContext.Set<T>().AsEnumerable();
}
public async Task<List<T>> ListAllAsync()
{
return await _dbContext.Set<T>().ToListAsync();
}
public IEnumerable<T> List(ISpecification<T> spec)
{
var queryableResultWithIncludes = spec.Includes
.Aggregate(_dbContext.Set<T>().AsQueryable(),
(current, include) => current.Include(include));
return queryableResultWithIncludes
var secondaryResult = spec.IncludeStrings
.Aggregate(queryableResultWithIncludes,
(current, include) => current.Include(include));
return secondaryResult
.Where(spec.Criteria)
.ToList();
.AsEnumerable();
}
public async Task<List<T>> ListAsync(ISpecification<T> spec)
{
var queryableResultWithIncludes = spec.Includes
.Aggregate(_dbContext.Set<T>().AsQueryable(),
(current, include) => current.Include(include));
var secondaryResult = spec.IncludeStrings
.Aggregate(queryableResultWithIncludes,
(current, include) => current.Include(include));
return await secondaryResult
.Where(spec.Criteria)
.ToListAsync();
}
public T Add(T entity)
@@ -43,10 +75,12 @@ namespace Infrastructure.Data
return entity;
}
public void Delete(T entity)
public async Task<T> AddAsync(T entity)
{
_dbContext.Set<T>().Remove(entity);
_dbContext.SaveChanges();
_dbContext.Set<T>().Add(entity);
await _dbContext.SaveChangesAsync();
return entity;
}
public void Update(T entity)
@@ -54,6 +88,21 @@ namespace Infrastructure.Data
_dbContext.Entry(entity).State = EntityState.Modified;
_dbContext.SaveChanges();
}
public async Task UpdateAsync(T entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
}
public void Delete(T entity)
{
_dbContext.Set<T>().Remove(entity);
_dbContext.SaveChanges();
}
public async Task DeleteAsync(T entity)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync();
}
}
}

View File

@@ -0,0 +1,31 @@
using ApplicationCore.Entities.OrderAggregate;
using ApplicationCore.Interfaces;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using System.Threading.Tasks;
namespace Infrastructure.Data
{
public class OrderRepository : EfRepository<Order>, IOrderRepository
{
public OrderRepository(CatalogContext dbContext) : base(dbContext)
{
}
public Order GetByIdWithItems(int id)
{
return _dbContext.Orders
.Include(o => o.OrderItems)
.Include("OrderItems.ItemOrdered")
.FirstOrDefault();
}
public Task<Order> GetByIdWithItemsAsync(int id)
{
return _dbContext.Orders
.Include(o => o.OrderItems)
.Include("OrderItems.ItemOrdered")
.FirstOrDefaultAsync();
}
}
}

View File

@@ -1,30 +0,0 @@
using ApplicationCore.Exceptions;
using ApplicationCore.Interfaces;
using Microsoft.AspNetCore.Hosting;
using System.IO;
namespace Infrastructure.FileSystem
{
public class LocalFileImageService : IImageService
{
private readonly IHostingEnvironment _env;
public LocalFileImageService(IHostingEnvironment env)
{
_env = env;
}
public byte[] GetImageBytesById(int id)
{
try
{
var contentRoot = _env.ContentRootPath + "//Pics";
var path = Path.Combine(contentRoot, id + ".png");
return File.ReadAllBytes(path);
}
catch (FileNotFoundException ex)
{
throw new CatalogImageMissingException(ex);
}
}
}
}

View File

@@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
namespace Infrastructure.Identity

View File

@@ -1,30 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.4</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.2" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="StructureMap.Microsoft.DependencyInjection" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationCore\ApplicationCore.csproj" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.1" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\Migrations\" />
<Folder Include="Services\" />
</ItemGroup>
</Project>

View File

@@ -10,9 +10,14 @@ namespace Infrastructure.Logging
{
_logger = loggerFactory.CreateLogger<T>();
}
public void LogWarning(string message, params object[] args)
{
_logger.LogWarning(message, args);
}
public void LogInformation(string message, params object[] args)
{
_logger.LogInformation(message, args);
}
}
}

View File

@@ -0,0 +1,40 @@
using ApplicationCore.Interfaces;
using ApplicationCore.Entities.OrderAggregate;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
namespace Infrastructure.Services
{
public class OrderService : IOrderService
{
private readonly IAsyncRepository<Order> _orderRepository;
private readonly IAsyncRepository<Basket> _basketRepository;
private readonly IAsyncRepository<CatalogItem> _itemRepository;
public OrderService(IAsyncRepository<Basket> basketRepository,
IAsyncRepository<CatalogItem> itemRepository,
IAsyncRepository<Order> orderRepository)
{
_orderRepository = orderRepository;
_basketRepository = basketRepository;
_itemRepository = itemRepository;
}
public async Task CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
var items = new List<OrderItem>();
foreach (var item in basket.Items)
{
var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri);
var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity);
items.Add(orderItem);
}
var order = new Order(basket.BuyerId, shippingAddress, items);
await _orderRepository.AddAsync(order);
}
}
}

View File

@@ -3,12 +3,11 @@ using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Infrastructure.Identity;
using System;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using ApplicationCore.Interfaces;
using Web;
using Microsoft.AspNetCore.Authentication;
namespace Microsoft.eShopWeb.Controllers
{
@@ -17,18 +16,15 @@ namespace Microsoft.eShopWeb.Controllers
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly string _externalCookieScheme;
private readonly IBasketService _basketService;
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IOptions<IdentityCookieOptions> identityCookieOptions,
IBasketService basketService)
{
_userManager = userManager;
_signInManager = signInManager;
_externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme;
_basketService = basketService;
}
@@ -37,9 +33,15 @@ namespace Microsoft.eShopWeb.Controllers
[AllowAnonymous]
public async Task<IActionResult> SignIn(string returnUrl = null)
{
await HttpContext.Authentication.SignOutAsync(_externalCookieScheme);
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ViewData["ReturnUrl"] = returnUrl;
if (!String.IsNullOrEmpty(returnUrl) &&
returnUrl.ToLower().Contains("checkout"))
{
ViewData["ReturnUrl"] = "/Basket/Index";
}
return View();
}
@@ -61,7 +63,7 @@ namespace Microsoft.eShopWeb.Controllers
string anonymousBasketId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (!String.IsNullOrEmpty(anonymousBasketId))
{
_basketService.TransferBasket(anonymousBasketId, model.Email);
await _basketService.TransferBasketAsync(anonymousBasketId, model.Email);
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);
}
return RedirectToLocal(returnUrl);
@@ -74,7 +76,6 @@ namespace Microsoft.eShopWeb.Controllers
[ValidateAntiForgeryToken]
public async Task<ActionResult> SignOut()
{
HttpContext.Session.Clear();
await _signInManager.SignOutAsync();
return RedirectToAction(nameof(CatalogController.Index), "Catalog");

View File

@@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Identity;
using Infrastructure.Identity;
using System;
using Web;
using System.Collections.Generic;
using ApplicationCore.Entities.OrderAggregate;
using Microsoft.AspNetCore.Authorization;
namespace Microsoft.eShopWeb.Controllers
{
@@ -17,14 +20,20 @@ namespace Microsoft.eShopWeb.Controllers
private const string _basketSessionKey = "basketId";
private readonly IUriComposer _uriComposer;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IAppLogger<BasketController> _logger;
private readonly IOrderService _orderService;
public BasketController(IBasketService basketService,
IOrderService orderService,
IUriComposer uriComposer,
SignInManager<ApplicationUser> signInManager)
SignInManager<ApplicationUser> signInManager,
IAppLogger<BasketController> logger)
{
_basketService = basketService;
_uriComposer = uriComposer;
_signInManager = signInManager;
_logger = logger;
_orderService = orderService;
}
[HttpGet]
@@ -35,6 +44,16 @@ namespace Microsoft.eShopWeb.Controllers
return View(basketModel);
}
[HttpPost]
public async Task<IActionResult> Index(Dictionary<string, int> items)
{
var basketViewModel = await GetBasketViewModelAsync();
await _basketService.SetQuantities(basketViewModel.Id, items);
return View(await GetBasketViewModelAsync());
}
// POST: /Basket/AddToBasket
[HttpPost]
public async Task<IActionResult> AddToBasket(CatalogItemViewModel productDetails)
@@ -51,11 +70,15 @@ namespace Microsoft.eShopWeb.Controllers
}
[HttpPost]
public async Task<IActionResult> Checkout()
[Authorize]
public async Task<IActionResult> Checkout(Dictionary<string, int> items)
{
var basket = await GetBasketViewModelAsync();
var basketViewModel = await GetBasketViewModelAsync();
await _basketService.SetQuantities(basketViewModel.Id, items);
await _basketService.Checkout(basket.Id);
await _orderService.CreateOrderAsync(basketViewModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240"));
await _basketService.DeleteBasketAsync(basketViewModel.Id);
return View("Checkout");
}

View File

@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.eShopWeb.ViewModels;
using System;
using ApplicationCore.Entities.OrderAggregate;
using ApplicationCore.Interfaces;
using System.Linq;
using ApplicationCore.Specifications;
namespace Microsoft.eShopWeb.Controllers
{
[Authorize]
[Route("[controller]/[action]")]
public class OrderController : Controller
{
private readonly IOrderRepository _orderRepository;
public OrderController(IOrderRepository orderRepository) {
_orderRepository = orderRepository;
}
public async Task<IActionResult> Index()
{
var orders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name));
var viewModel = orders
.Select(o => new OrderViewModel()
{
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
{
Discount = 0,
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = o.Id,
ShippingAddress = o.ShipToAddress,
Status = "Pending",
Total = o.Total()
});
return View(viewModel);
}
[HttpGet("{orderId}")]
public async Task<IActionResult> Detail(int orderId)
{
var order = await _orderRepository.GetByIdWithItemsAsync(orderId);
var viewModel = new OrderViewModel()
{
OrderDate = order.OrderDate,
OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel()
{
Discount = 0,
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = order.Id,
ShippingAddress = order.ShipToAddress,
Status = "Pending",
Total = order.Total()
};
return View(viewModel);
}
private OrderViewModel GetOrder()
{
var order = new OrderViewModel()
{
OrderDate = DateTimeOffset.Now.AddDays(-1),
OrderNumber = 12354,
Status = "Submitted",
Total = 123.45m,
ShippingAddress = new Address("123 Main St.", "Kent", "OH", "United States", "44240")
};
order.OrderItems.Add(new OrderItemViewModel()
{
ProductId = 1,
PictureUrl = "",
ProductName = "Something",
UnitPrice = 5.05m,
Units = 2
});
return order;
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.eShopWeb.ViewModels;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ApplicationCore.Interfaces
@@ -6,8 +7,9 @@ namespace ApplicationCore.Interfaces
public interface IBasketService
{
Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task TransferBasket(string anonymousId, string userName);
Task TransferBasketAsync(string anonymousId, string userName);
Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity);
Task Checkout(int basketId);
Task SetQuantities(int basketId, Dictionary<string, int> quantities);
Task DeleteBasketAsync(int basketId);
}
}

View File

@@ -1,28 +1,20 @@
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore;
namespace Microsoft.eShopWeb
{
public class Program
{
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseUrls("http://0.0.0.0:5106")
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureLogging(factory =>
{
factory.AddConsole(LogLevel.Warning);
factory.AddDebug();
})
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseUrls("http://0.0.0.0:5106")
.UseStartup<Startup>()
.Build();
}
}

View File

@@ -1,7 +1,6 @@
using ApplicationCore.Interfaces;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System;
using System.Linq;
using Microsoft.eShopWeb.ViewModels;
using System.Collections.Generic;
@@ -11,23 +10,26 @@ namespace Web.Services
{
public class BasketService : IBasketService
{
private readonly IRepository<Basket> _basketRepository;
private readonly IAsyncRepository<Basket> _basketRepository;
private readonly IUriComposer _uriComposer;
private readonly IAppLogger<BasketService> _logger;
private readonly IRepository<CatalogItem> _itemRepository;
public BasketService(IRepository<Basket> basketRepository,
public BasketService(IAsyncRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer)
IUriComposer uriComposer,
IAppLogger<BasketService> logger)
{
_basketRepository = basketRepository;
_uriComposer = uriComposer;
this._logger = logger;
_itemRepository = itemRepository;
}
public async Task<BasketViewModel> GetOrCreateBasketForUser(string userName)
{
var basketSpec = new BasketWithItemsSpecification(userName);
var basket = _basketRepository.List(basketSpec).FirstOrDefault();
var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault();
if(basket == null)
{
@@ -63,7 +65,7 @@ namespace Web.Services
public async Task<BasketViewModel> CreateBasketForUser(string userId)
{
var basket = new Basket() { BuyerId = userId };
_basketRepository.Add(basket);
await _basketRepository.AddAsync(basket);
return new BasketViewModel()
{
@@ -75,30 +77,41 @@ namespace Web.Services
public async Task AddItemToBasket(int basketId, int catalogItemId, decimal price, int quantity)
{
var basket = _basketRepository.GetById(basketId);
var basket = await _basketRepository.GetByIdAsync(basketId);
basket.AddItem(catalogItemId, price, quantity);
_basketRepository.Update(basket);
await _basketRepository.UpdateAsync(basket);
}
public async Task Checkout(int basketId)
public async Task SetQuantities(int basketId, Dictionary<string,int> quantities)
{
var basket = _basketRepository.GetById(basketId);
// TODO: Actually Process the order
_basketRepository.Delete(basket);
var basket = await _basketRepository.GetByIdAsync(basketId);
foreach (var item in basket.Items)
{
if (quantities.TryGetValue(item.Id.ToString(), out var quantity))
{
_logger.LogWarning($"Updating quantity of item ID:{item.Id} to {quantity}.");
item.Quantity = quantity;
}
}
await _basketRepository.UpdateAsync(basket);
}
public Task TransferBasket(string anonymousId, string userName)
public async Task DeleteBasketAsync(int basketId)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
await _basketRepository.DeleteAsync(basket);
}
public async Task TransferBasketAsync(string anonymousId, string userName)
{
var basketSpec = new BasketWithItemsSpecification(anonymousId);
var basket = _basketRepository.List(basketSpec).FirstOrDefault();
if (basket == null) return Task.CompletedTask;
var basket = (await _basketRepository.ListAsync(basketSpec)).FirstOrDefault();
if (basket == null) return;
basket.BuyerId = userName;
_basketRepository.Update(basket);
return Task.CompletedTask;
await _basketRepository.UpdateAsync(basket);
}
}
}

View File

@@ -15,15 +15,15 @@ namespace Microsoft.eShopWeb.Services
{
private readonly ILogger<CatalogService> _logger;
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IRepository<CatalogBrand> _brandRepository;
private readonly IRepository<CatalogType> _typeRepository;
private readonly IAsyncRepository<CatalogBrand> _brandRepository;
private readonly IAsyncRepository<CatalogType> _typeRepository;
private readonly IUriComposer _uriComposer;
public CatalogService(
ILoggerFactory loggerFactory,
IRepository<CatalogItem> itemRepository,
IRepository<CatalogBrand> brandRepository,
IRepository<CatalogType> typeRepository,
IAsyncRepository<CatalogBrand> brandRepository,
IAsyncRepository<CatalogType> typeRepository,
IUriComposer uriComposer)
{
_logger = loggerFactory.CreateLogger<CatalogService>();
@@ -83,7 +83,7 @@ namespace Microsoft.eShopWeb.Services
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
_logger.LogInformation("GetBrands called.");
var brands = _brandRepository.List();
var brands = await _brandRepository.ListAllAsync();
var items = new List<SelectListItem>
{
@@ -100,7 +100,7 @@ namespace Microsoft.eShopWeb.Services
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
_logger.LogInformation("GetTypes called.");
var types = _typeRepository.List();
var types = await _typeRepository.ListAllAsync();
var items = new List<SelectListItem>
{
new SelectListItem() { Value = null, Text = "All", Selected = true }

View File

@@ -1,41 +1,35 @@
using Microsoft.eShopWeb.Services;
using ApplicationCore.Entities.OrderAggregate;
using ApplicationCore.Interfaces;
using ApplicationCore.Services;
using Infrastructure.Data;
using Infrastructure.Identity;
using Infrastructure.Logging;
using Infrastructure.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.eShopWeb.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System;
using System.Text;
using Microsoft.AspNetCore.Http;
using ApplicationCore.Interfaces;
using Infrastructure.FileSystem;
using Infrastructure.Logging;
using Microsoft.AspNetCore.Identity;
using Web.Services;
using ApplicationCore.Services;
using Infrastructure.Data;
namespace Microsoft.eShopWeb
{
public class Startup
{
private IServiceCollection _services;
public Startup(IHostingEnvironment env)
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
Configuration = configuration;
}
public IConfigurationRoot Configuration { get; }
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Requires LocalDB which can be installed with SQL Server Express 2016
@@ -46,11 +40,6 @@ namespace Microsoft.eShopWeb
{
c.UseInMemoryDatabase("Catalog");
//c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection"));
c.ConfigureWarnings(wb =>
{
//By default, in this application, we don't want to have client evaluations
wb.Log(RelationalEventId.QueryClientEvaluationWarning);
});
}
catch (System.Exception ex )
{
@@ -67,21 +56,28 @@ namespace Microsoft.eShopWeb
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromHours(1);
options.LoginPath = "/Account/Signin";
options.LogoutPath = "/Account/Signout";
});
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>));
services.AddMemoryCache();
services.AddScoped<ICatalogService, CachedCatalogService>();
services.AddScoped<IBasketService, BasketService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<CatalogService>();
services.Configure<CatalogSettings>(Configuration);
services.AddSingleton<IUriComposer>(new UriComposer(Configuration.Get<CatalogSettings>()));
// TODO: Remove
services.AddSingleton<IImageService, LocalFileImageService>();
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
// Add memory cache services
services.AddMemoryCache();
@@ -98,7 +94,22 @@ namespace Microsoft.eShopWeb
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
ListAllRegisteredServices(app);
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Catalog/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc();
}
private void ListAllRegisteredServices(IApplicationBuilder app)
{
app.Map("/allservices", builder => builder.Run(async context =>
{
var sb = new StringBuilder();
@@ -118,26 +129,17 @@ namespace Microsoft.eShopWeb
await context.Response.WriteAsync(sb.ToString());
}));
}
else
{
app.UseExceptionHandler("/Catalog/Error");
}
app.UseStaticFiles();
app.UseIdentity();
app.UseMvc();
}
public void ConfigureDevelopment(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
UserManager<ApplicationUser> userManager)
UserManager<ApplicationUser> userManager,
CatalogContext catalogContext)
{
Configure(app, env);
//Seed Data
CatalogContextSeed.SeedAsync(app, loggerFactory)
CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory)
.Wait();
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
@@ -155,17 +157,17 @@ namespace Microsoft.eShopWeb
public void ConfigureProduction(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
UserManager<ApplicationUser> userManager)
UserManager<ApplicationUser> userManager,
CatalogContext catalogContext)
{
Configure(app, env);
//Seed Data
CatalogContextSeed.SeedAsync(app, loggerFactory)
CatalogContextSeed.SeedAsync(app, catalogContext, loggerFactory)
.Wait();
var defaultUser = new ApplicationUser { UserName = "demouser@microsoft.com", Email = "demouser@microsoft.com" };
userManager.CreateAsync(defaultUser, "Pass@word1").Wait();
}
}
}

View File

@@ -0,0 +1,52 @@
using ApplicationCore.Interfaces;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ViewModels;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace Web.ViewComponents
{
public class Basket : ViewComponent
{
private readonly IBasketService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
public Basket(IBasketService basketService,
SignInManager<ApplicationUser> signInManager)
{
_basketService = basketService;
_signInManager = signInManager;
}
public async Task<IViewComponentResult> InvokeAsync(string userName)
{
var vm = new BasketComponentViewModel();
vm.ItemsCount = (await GetBasketViewModelAsync()).Items.Sum(i => i.Quantity);
return View(vm);
}
private async Task<BasketViewModel> GetBasketViewModelAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
return await _basketService.GetOrCreateBasketForUser(User.Identity.Name);
}
string anonymousId = GetBasketIdFromCookie();
if (anonymousId == null) return new BasketViewModel();
return await _basketService.GetOrCreateBasketForUser(anonymousId);
}
private string GetBasketIdFromCookie()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
return Request.Cookies[Constants.BASKET_COOKIENAME];
}
return null;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Microsoft.eShopWeb.ViewModels
{
public class BasketComponentViewModel
{
public int ItemsCount { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.ViewModels
{
public class OrderItemViewModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount { get; set; }
public int Units { get; set; }
public string PictureUrl { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using ApplicationCore.Entities.OrderAggregate;
using System;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.ViewModels
{
public class OrderViewModel
{
public int OrderNumber { get; set; }
public DateTimeOffset OrderDate { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
public Address ShippingAddress { get; set; }
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>();
}
}

View File

@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.ViewModels
{

View File

@@ -1,7 +1,7 @@
@using Microsoft.eShopWeb.ViewModels
@model BasketViewModel
@{
ViewData["Title"] = "Basket";
@model BasketViewModel
}
<section class="esh-catalog-hero">
<div class="container">
@@ -23,8 +23,9 @@
<section class="esh-basket-title col-xs-2">Cost</section>
</article>
<div class="esh-catalog-items row">
@foreach (var item in Model.Items)
@for (int i=0; i< Model.Items.Count; i++)
{
var item = Model.Items[i];
<article class="esh-basket-items row">
<div>
<section class="esh-basket-item esh-basket-item--middle col-lg-3 hidden-lg-down">
@@ -33,8 +34,8 @@
<section class="esh-basket-item esh-basket-item--middle col-xs-3">@item.ProductName</section>
<section class="esh-basket-item esh-basket-item--middle col-xs-2">$ @item.UnitPrice.ToString("N2")</section>
<section class="esh-basket-item esh-basket-item--middle col-xs-2">
<input type="hidden" name="@("quantities[" + item.Id +"].Key")" value="@item.Id" />
<input type="number" class="esh-basket-input" min="1" name="@("quantities[" + item.Id +"].Value")" value="@item.Quantity" />
<input type="hidden" name="@("Items[" + i + "].Key")" value="@item.Id" />
<input type="number" class="esh-basket-input" min="1" name="@("Items[" + i + "].Value")" value="@item.Quantity" />
</section>
<section class="esh-basket-item esh-basket-item--middle esh-basket-item--mark col-xs-2">$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")</section>
</div>
@@ -65,7 +66,9 @@
</article>
</div>
}
<section class="esh-basket-item col-xs-push-9 col-xs-3">
<section class="esh-basket-item col-xs-push-8 col-xs-4">
<button class="btn esh-basket-checkout" name="updatebutton" value="" type="submit"
asp-action="Update">[ Update ]</button>
<input type="submit" asp-action="Checkout"
class="btn esh-basket-checkout"
value="[ Checkout ]" name="action" />

View File

@@ -0,0 +1,88 @@
@using Microsoft.eShopWeb.ViewModels
@model OrderViewModel
@{
ViewData["Title"] = "My Order History";
}
@{
ViewData["Title"] = "Order Detail";
}
<div class="esh-orders_detail">
<div class="container">
<section class="esh-orders_detail-section">
<article class="esh-orders_detail-titles row">
<section class="esh-orders_detail-title col-xs-3">Order number</section>
<section class="esh-orders_detail-title col-xs-3">Date</section>
<section class="esh-orders_detail-title col-xs-3">Total</section>
<section class="esh-orders_detail-title col-xs-3">Status</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-3">@Model.OrderNumber</section>
<section class="esh-orders_detail-item col-xs-3">@Model.OrderDate</section>
<section class="esh-orders_detail-item col-xs-3">$@Model.Total</section>
<section class="esh-orders_detail-title col-xs-3">@Model.Status</section>
</article>
</section>
@*<section class="esh-orders_detail-section">
<article class="esh-orders_detail-titles row">
<section class="esh-orders_detail-title col-xs-12">Description</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-12">@Model.Description</section>
</article>
</section>*@
<section class="esh-orders_detail-section">
<article class="esh-orders_detail-titles row">
<section class="esh-orders_detail-title col-xs-12">Shipping Address</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.Street</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.City</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.Country</section>
</article>
</section>
<section class="esh-orders_detail-section">
<article class="esh-orders_detail-titles row">
<section class="esh-orders_detail-title col-xs-12">ORDER DETAILS</section>
</article>
@for (int i = 0; i < Model.OrderItems.Count; i++)
{
var item = Model.OrderItems[i];
<article class="esh-orders_detail-items esh-orders_detail-items--border row">
<section class="esh-orders_detail-item col-md-4 hidden-md-down">
<img class="esh-orders_detail-image" src="@item.PictureUrl">
</section>
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-4">@item.ProductName</section>
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">$ @item.UnitPrice.ToString("N2")</section>
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">@item.Units</section>
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section>
</article>
}
</section>
<section class="esh-orders_detail-section esh-orders_detail-section--right">
<article class="esh-orders_detail-titles esh-basket-titles--clean row">
<section class="esh-orders_detail-title col-xs-9"></section>
<section class="esh-orders_detail-title col-xs-2">TOTAL</section>
</article>
<article class="esh-orders_detail-items row">
<section class="esh-orders_detail-item col-xs-9"></section>
<section class="esh-orders_detail-item esh-orders_detail-item--mark col-xs-2">$ @Model.Total</section>
</article>
</section>
</div>
</div>

View File

@@ -0,0 +1,39 @@
@using Microsoft.eShopWeb.ViewModels
@model IEnumerable<OrderViewModel>
@{
ViewData["Title"] = "My Order History";
}
<div class="esh-orders">
<div class="container">
<h1>@ViewData["Title"]</h1>
<article class="esh-orders-titles row">
<section class="esh-orders-title col-xs-2">Order number</section>
<section class="esh-orders-title col-xs-4">Date</section>
<section class="esh-orders-title col-xs-2">Total</section>
<section class="esh-orders-title col-xs-2">Status</section>
<section class="esh-orders-title col-xs-2"></section>
</article>
@if (Model != null && Model.Any())
{
@foreach (var item in Model)
{
<article class="esh-orders-items row">
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section>
<section class="esh-orders-item col-xs-4">@Html.DisplayFor(modelItem => item.OrderDate)</section>
<section class="esh-orders-item col-xs-2">$ @Html.DisplayFor(modelItem => item.Total)</section>
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.Status)</section>
<section class="esh-orders-item col-xs-1">
<a class="esh-orders-link" asp-controller="Order" asp-action="Detail" asp-route-orderId="@item.OrderNumber">Detail</a>
</section>
<section class="esh-orders-item col-xs-1">
@if (item.Status.ToLower() == "submitted")
{
<a class="esh-orders-link" asp-controller="Order" asp-action="cancel" asp-route-orderId="@item.OrderNumber">Cancel</a>
}
</section>
</article>
}
}
</div>
</div>

View File

@@ -0,0 +1,17 @@
@model BasketComponentViewModel
@{
ViewData["Title"] = "My Basket";
}
<a class="esh-basketstatus "
asp-area=""
asp-controller="Basket"
asp-action="Index">
<div class="esh-basketstatus-image">
<img src="~/images/cart.png" />
</div>
<div class="esh-basketstatus-badge">
@Model.ItemsCount
</div>
</a>

View File

@@ -15,8 +15,12 @@
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/app.min.css" asp-append-version="true" />
</environment>
<link rel="stylesheet" href="~/css/app.component.css" />
<link rel="stylesheet" href="~/css/basket/basket.component.css" />
<link rel="stylesheet" href="~/css/catalog/pager.css" />
<link rel="stylesheet" href="~/css/catalog/catalog.component.css" />
<link rel="stylesheet" href="~/css/basket/basket-status/basket-status.component.css" />
<link rel="stylesheet" href="~/css/orders/orders.component.css" />
</head>
<body>
<header class="navbar navbar-light navbar-static-top">
@@ -43,7 +47,6 @@
<article class="row">
<section class="col-sm-6">
<img class="esh-app-footer-brand" src="../images/brand_dark.png" />
</section>
<section class="col-sm-6">

View File

@@ -1,5 +1,4 @@
@using Microsoft.AspNetCore.Identity
@*@inject IIdentityParser<ApplicationUser> UserManager*@
@if (Context.User.Identity.IsAuthenticated)
{
@@ -14,13 +13,13 @@
<section class="esh-identity-drop">
@*<a class="esh-identity-item"
<a class="esh-identity-item"
asp-controller="Order"
asp-action="Index">
<div class="esh-identity-name esh-identity-name--upper">My orders</div>
<img class="esh-identity-image" src="~/images/my_orders.png">
</a>*@
</a>
<a class="esh-identity-item"
href="javascript:document.getElementById('logoutForm').submit()">
@@ -33,9 +32,9 @@
</div>
</section>
@*<section class="col-lg-1 col-xs-12">
@await Component.InvokeAsync("Basket", new { user = UserManager.Parse(User) })
</section>*@
<section class="col-lg-1 col-xs-12">
@await Component.InvokeAsync("Basket", User.Identity.Name)
</section>
}
else
@@ -53,5 +52,7 @@ else
</div>
</section>
@*<section class="col-lg-1 col-xs-12"></section>*@
<section class="col-lg-1 col-xs-12">
@await Component.InvokeAsync("Basket")
</section>
}

View File

@@ -1,41 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81;</PackageTargetFallback>
<RuntimeFrameworkVersion>1.1.0</RuntimeFrameworkVersion>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Pics\**" />
<Content Remove="Pics\**" />
<EmbeddedResource Remove="Pics\**" />
<None Remove="Pics\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="1.50.2" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Session" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="1.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="1.1.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
<PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Folder Include="Views\Catalog\" />
<Folder Include="wwwroot\css\catalog\" />
<Folder Include="wwwroot\fonts\" />
</ItemGroup>
<ItemGroup>
@@ -43,9 +23,9 @@
<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.1" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" />
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" />
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\images\products\1.png" />

View File

@@ -0,0 +1,42 @@
[
{
"outputFile": "wwwroot/css/orders/orders.component.css",
"inputFile": "wwwroot/css/orders/orders.component.scss"
},
//{
// "outputFile": "wwwroot/css/orders/orders-new/orders-new.component.css",
// "inputFile": "wwwroot/css/orders/orders-new/orders-new.component.scss"
//},
//{
// "outputFile": "wwwroot/css/orders/orders-detail/orders-detail.component.css",
// "inputFile": "wwwroot/css/orders/orders-detail/orders-detail.component.scss"
//},
{
"outputFile": "wwwroot/css/catalog/catalog.component.css",
"inputFile": "wwwroot/css/catalog/catalog.component.scss"
},
{
"outputFile": "wwwroot/css/basket/basket.component.css",
"inputFile": "wwwroot/css/basket/basket.component.scss"
},
{
"outputFile": "wwwroot/css/basket/basket-status/basket-status.component.css",
"inputFile": "wwwroot/css/basket/basket-status/basket-status.component.scss"
},
//{
// "outputFile": "wwwroot/css/shared/components/header/header.css",
// "inputFile": "wwwroot/css/shared/components/header/header.scss"
//},
//{
// "outputFile": "wwwroot/css/shared/components/identity/identity.css",
// "inputFile": "wwwroot/css/shared/components/identity/identity.scss"
//},
//{
// "outputFile": "wwwroot/css/shared/components/pager/pager.css",
// "inputFile": "wwwroot/css/shared/components/pager/pager.scss"
//},
{
"outputFile": "wwwroot/css/app.component.css",
"inputFile": "wwwroot/css/app.component.scss"
}
]

View File

@@ -0,0 +1,65 @@
// Colors
$color-brand: #00A69C;
$color-brand-dark: darken($color-brand, 10%);
$color-brand-darker: darken($color-brand, 20%);
$color-brand-bright: lighten($color-brand, 10%);
$color-brand-brighter: lighten($color-brand, 20%);
$color-secondary: #83D01B;
$color-secondary-dark: darken($color-secondary, 5%);
$color-secondary-darker: darken($color-secondary, 20%);
$color-secondary-bright: lighten($color-secondary, 10%);
$color-secondary-brighter: lighten($color-secondary, 20%);
$color-warning: #ff0000;
$color-warning-dark: darken($color-warning, 5%);
$color-warning-darker: darken($color-warning, 20%);
$color-warning-bright: lighten($color-warning, 10%);
$color-warning-brighter: lighten($color-warning, 20%);
$color-background-dark: #333333;
$color-background-darker: #000000;
$color-background-bright: #EEEEFF;
$color-background-brighter: #FFFFFF;
$color-foreground-dark: #333333;
$color-foreground-darker: #000000;
$color-foreground-bright: #EEEEEE;
$color-foreground-brighter: #FFFFFF;
// Animations
$animation-speed-default: .35s;
$animation-speed-slow: .5s;
$animation-speed-fast: .15s;
// Fonts
$font-weight-light: 200;
$font-weight-semilight: 300;
$font-weight-normal: 400;
$font-weight-semibold: 600;
$font-weight-bold: 700;
$font-size-xs: .65rem; // 10.4px
$font-size-s: .85rem; // 13.6px
$font-size-m: 1rem; // 16px
$font-size-l: 1.25rem; // 20px
$font-size-xl: 1.5rem; // 24px
// Medias
$media-screen-xxs: 360px;
$media-screen-xs: 640px;
$media-screen-s: 768px;
$media-screen-m: 1024px;
$media-screen-l: 1280px;
$media-screen-xl: 1440px;
$media-screen-xxl: 1680px;
$media-screen-xxxl: 1920px;
// Borders
$border-light: 1px;
// Images
$image_path: '../../images/';
$image-main_banner: '#{$image_path}main_banner.png';
$image-arrow_down: '#{$image_path}arrow-down.png';

View File

@@ -0,0 +1,11 @@
.esh-app-footer {
background-color: #000000;
border-top: 1px solid #EEEEEE;
margin-top: 2.5rem;
padding-bottom: 2.5rem;
padding-top: 2.5rem;
width: 100%; }
.esh-app-footer-brand {
height: 50px;
width: 230px; }

View File

@@ -0,0 +1 @@
.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%}.esh-app-footer-brand{height:50px;width:230px}

View File

@@ -0,0 +1,23 @@
@import './variables';
.esh-app {
&-footer {
$margin: 2.5rem;
$padding: 2.5rem;
background-color: $color-background-darker;
border-top: $border-light solid $color-foreground-bright;
margin-top: $margin;
padding-bottom: $padding;
padding-top: $padding;
width: 100%;
$height: 50px;
&-brand {
height: $height;
width: 230px;
}
}
}

View File

@@ -0,0 +1,43 @@
.esh-basketstatus {
cursor: pointer;
display: inline-block;
float: right;
position: relative;
transition: all 0.35s; }
.esh-basketstatus.is-disabled {
opacity: .5;
pointer-events: none; }
.esh-basketstatus-image {
height: 36px;
margin-top: .5rem; }
.esh-basketstatus-badge {
background-color: #83D01B;
border-radius: 50%;
color: #FFFFFF;
display: block;
height: 1.5rem;
left: 50%;
position: absolute;
text-align: center;
top: 0;
transform: translateX(-38%);
transition: all 0.35s;
width: 1.5rem; }
.esh-basketstatus-badge-inoperative {
background-color: #ff0000;
border-radius: 50%;
color: #FFFFFF;
display: block;
height: 1.5rem;
left: 50%;
position: absolute;
text-align: center;
top: 0;
transform: translateX(-38%);
transition: all 0.35s;
width: 1.5rem; }
.esh-basketstatus:hover .esh-basketstatus-badge {
background-color: transparent;
color: #75b918;
transition: all 0.35s; }

View File

@@ -0,0 +1 @@
.esh-basketstatus{cursor:pointer;display:inline-block;float:right;position:relative;transition:all .35s}.esh-basketstatus.is-disabled{opacity:.5;pointer-events:none}.esh-basketstatus-image{height:36px;margin-top:.5rem}.esh-basketstatus-badge{background-color:#83d01b;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus-badge-inoperative{background-color:#f00;border-radius:50%;color:#fff;display:block;height:1.5rem;left:50%;position:absolute;text-align:center;top:0;transform:translateX(-38%);transition:all .35s;width:1.5rem}.esh-basketstatus:hover .esh-basketstatus-badge{background-color:transparent;color:#75b918;transition:all .35s}

View File

@@ -0,0 +1,57 @@
@import '../../variables';
.esh-basketstatus {
cursor: pointer;
display: inline-block;
float: right;
position: relative;
transition: all $animation-speed-default;
&.is-disabled {
opacity: .5;
pointer-events: none;
}
&-image {
height: 36px;
margin-top: .5rem;
}
&-badge {
$size: 1.5rem;
background-color: $color-secondary;
border-radius: 50%;
color: $color-foreground-brighter;
display: block;
height: $size;
left: 50%;
position: absolute;
text-align: center;
top: 0;
transform: translateX(-38%);
transition: all $animation-speed-default;
width: $size;
}
&-badge-inoperative {
$size: 1.5rem;
background-color: $color-warning;
border-radius: 50%;
color: $color-foreground-brighter;
display: block;
height: $size;
left: 50%;
position: absolute;
text-align: center;
top: 0;
transform: translateX(-38%);
transition: all $animation-speed-default;
width: $size;
}
&:hover &-badge {
background-color: transparent;
color: $color-secondary-dark;
transition: all $animation-speed-default;
}
}

View File

@@ -0,0 +1,49 @@
.esh-basket {
min-height: 80vh; }
.esh-basket-titles {
padding-bottom: 1rem;
padding-top: 2rem; }
.esh-basket-titles--clean {
padding-bottom: 0;
padding-top: 0; }
.esh-basket-title {
text-transform: uppercase; }
.esh-basket-items--border {
border-bottom: 1px solid #EEEEEE;
padding: .5rem 0; }
.esh-basket-items--border:last-of-type {
border-color: transparent; }
.esh-basket-items-margin-left1 {
margin-left: 1px; }
.esh-basket-item {
font-size: 1rem;
font-weight: 300; }
.esh-basket-item--middle {
line-height: 8rem; }
@media screen and (max-width: 1024px) {
.esh-basket-item--middle {
line-height: 1rem; } }
.esh-basket-item--mark {
color: #00A69C; }
.esh-basket-image {
height: 8rem; }
.esh-basket-input {
line-height: 1rem;
width: 100%; }
.esh-basket-checkout {
background-color: #83D01B;
border: 0;
border-radius: 0;
color: #FFFFFF;
display: inline-block;
font-size: 1rem;
font-weight: 400;
margin-top: 1rem;
padding: 1rem 1.5rem;
text-align: center;
text-transform: uppercase;
transition: all 0.35s; }
.esh-basket-checkout:hover {
background-color: #4a760f;
transition: all 0.35s; }

View File

@@ -0,0 +1 @@
.esh-basket{min-height:80vh}.esh-basket-titles{padding-bottom:1rem;padding-top:2rem}.esh-basket-titles--clean{padding-bottom:0;padding-top:0}.esh-basket-title{text-transform:uppercase}.esh-basket-items--border{border-bottom:1px solid #eee;padding:.5rem 0}.esh-basket-items--border:last-of-type{border-color:transparent}.esh-basket-items-margin-left1{margin-left:1px}.esh-basket-item{font-size:1rem;font-weight:300}.esh-basket-item--middle{line-height:8rem}@media screen and (max-width:1024px){.esh-basket-item--middle{line-height:1rem}}.esh-basket-item--mark{color:#00a69c}.esh-basket-image{height:8rem}.esh-basket-input{line-height:1rem;width:100%}.esh-basket-checkout{background-color:#83d01b;border:0;border-radius:0;color:#fff;display:inline-block;font-size:1rem;font-weight:400;margin-top:1rem;padding:1rem 1.5rem;text-align:center;text-transform:uppercase;transition:all .35s}.esh-basket-checkout:hover{background-color:#4a760f;transition:all .35s}

View File

@@ -0,0 +1,89 @@
@import '../variables';
@mixin margin-left($distance) {
margin-left: $distance;
}
.esh-basket {
min-height: 80vh;
&-titles {
padding-bottom: 1rem;
padding-top: 2rem;
&--clean {
padding-bottom: 0;
padding-top: 0;
}
}
&-title {
text-transform: uppercase;
}
&-items {
&--border {
border-bottom: $border-light solid $color-foreground-bright;
padding: .5rem 0;
&:last-of-type {
border-color: transparent;
}
}
&-margin-left1 {
@include margin-left(1px);
}
}
$item-height: 8rem;
&-item {
font-size: $font-size-m;
font-weight: $font-weight-semilight;
&--middle {
line-height: $item-height;
@media screen and (max-width: $media-screen-m) {
line-height: $font-size-m;
}
}
&--mark {
color: $color-brand;
}
}
&-image {
height: $item-height;
}
&-input {
line-height: 1rem;
width: 100%;
}
&-checkout {
background-color: $color-secondary;
border: 0;
border-radius: 0;
color: $color-foreground-brighter;
display: inline-block;
font-size: 1rem;
font-weight: $font-weight-normal;
margin-top: 1rem;
padding: 1rem 1.5rem;
text-align: center;
text-transform: uppercase;
transition: all $animation-speed-default;
&:hover {
background-color: $color-secondary-darker;
transition: all $animation-speed-default;
}
}
}

View File

@@ -1,228 +1,117 @@
.esh-catalog-hero {
.esh-catalog-hero {
background-image: url("../../images/main_banner.png");
background-size: cover;
height: 260px;
width: 100%;
}
width: 100%; }
.esh-catalog-title {
position: relative;
top: 74.28571px;
}
top: 74.28571px; }
.esh-catalog-filters {
background-color: #00A69C;
height: 65px;
}
height: 65px; }
.esh-catalog-filter {
-webkit-appearance: none;
background-color: transparent;
border-color: #00d9cc;
color: #FFFFFF;
cursor: pointer;
margin-right: 1rem;
margin-top: .5rem;
min-width: 140px;
outline-color: #83D01B;
padding-bottom: 0;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 1.5rem;
min-width: 140px;
-webkit-appearance: none;
}
padding-top: 1.5rem; }
.esh-catalog-filter option {
background-color: #00A69C;
}
background-color: #00A69C; }
.esh-catalog-label {
display: inline-block;
position: relative;
z-index: 0;
}
z-index: 0; }
.esh-catalog-label::before {
color: rgba(255, 255, 255, 0.5);
content: attr(data-title);
font-size: 0.65rem;
margin-top: 0.65rem;
margin-left: 0.5rem;
margin-top: 0.65rem;
position: absolute;
text-transform: uppercase;
z-index: 1;
}
z-index: 1; }
.esh-catalog-label::after {
background-image: url("../../images/arrow-down.png");
height: 7px;
content: '';
height: 7px;
position: absolute;
right: 1.5rem;
top: 2.5rem;
width: 10px;
z-index: 1;
}
z-index: 1; }
.esh-catalog-send {
background-color: #83D01B;
color: #FFFFFF;
cursor: pointer;
font-size: 1rem;
transform: translateY(.5rem);
margin-top: -1.5rem;
padding: 0.5rem;
transition: all 0.35s;
}
transition: all 0.35s; }
.esh-catalog-send:hover {
background-color: #4a760f;
transition: all 0.35s;
}
transition: all 0.35s; }
.esh-catalog-items {
margin-top: 1rem;
}
margin-top: 1rem; }
.esh-catalog-item {
text-align: center;
margin-bottom: 1.5rem;
text-align: center;
width: 33%;
display: inline-block;
float: none !important;
}
@media screen and (max-width: 1024px) {
float: none !important; }
@media screen and (max-width: 1024px) {
.esh-catalog-item {
width: 50%;
}
}
@media screen and (max-width: 768px) {
width: 50%; } }
@media screen and (max-width: 768px) {
.esh-catalog-item {
width: 100%;
}
}
width: 100%; } }
.esh-catalog-thumbnail {
max-width: 370px;
width: 100%;
}
width: 100%; }
.esh-catalog-button {
background-color: #83D01B;
border: none;
border: 0;
color: #FFFFFF;
cursor: pointer;
font-size: 1rem;
height: 3rem;
margin-top: 1rem;
transition: all 0.35s;
width: 80%;
}
width: 80%; }
.esh-catalog-button.is-disabled {
opacity: .5;
pointer-events: none;
}
pointer-events: none; }
.esh-catalog-button:hover {
background-color: #4a760f;
transition: all 0.35s;
}
transition: all 0.35s; }
.esh-catalog-name {
font-size: 1rem;
font-weight: 300;
margin-top: .5rem;
text-align: center;
text-transform: uppercase;
}
text-transform: uppercase; }
.esh-catalog-price {
text-align: center;
font-weight: 900;
font-size: 28px;
}
font-weight: 900;
text-align: center; }
.esh-catalog-price::before {
content: '$';
}
content: '$'; }
.esh-basket {
min-height: 80vh;
}
.esh-basket-titles {
padding-bottom: 1rem;
padding-top: 2rem;
}
.esh-basket-titles--clean {
padding-bottom: 0;
padding-top: 0;
}
.esh-basket-title {
text-transform: uppercase;
}
.esh-basket-items--border {
border-bottom: 1px solid #EEEEEE;
padding: .5rem 0;
}
.esh-basket-items--border:last-of-type {
border-color: transparent;
}
.esh-basket-items-margin-left1 {
margin-left: 1px;
}
.esh-basket-item {
font-size: 1rem;
font-weight: 300;
}
.esh-basket-item--middle {
line-height: 8rem;
}
@media screen and (max-width: 1024px) {
.esh-basket-item--middle {
line-height: 1rem;
}
}
.esh-basket-item--mark {
color: #00A69C;
}
.esh-basket-image {
height: 8rem;
}
.esh-basket-input {
line-height: 1rem;
width: 100%;
}
.esh-basket-checkout {
background-color: #83D01B;
border: 0;
border-radius: 0;
color: #FFFFFF;
display: inline-block;
font-size: 1rem;
font-weight: 400;
margin-top: 1rem;
padding: 1rem 1.5rem;
text-align: center;
text-transform: uppercase;
transition: all 0.35s;
}
.esh-basket-checkout:hover {
background-color: #4a760f;
transition: all 0.35s;
}

View File

@@ -0,0 +1 @@
.esh-catalog-hero{background-image:url("../../images/main_banner.png");background-size:cover;height:260px;width:100%}.esh-catalog-title{position:relative;top:74.28571px}.esh-catalog-filters{background-color:#00a69c;height:65px}.esh-catalog-filter{-webkit-appearance:none;background-color:transparent;border-color:#00d9cc;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#83d01b;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem}.esh-catalog-filter option{background-color:#00a69c}.esh-catalog-label{display:inline-block;position:relative;z-index:0}.esh-catalog-label::before{color:rgba(255,255,255,.5);content:attr(data-title);font-size:.65rem;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1}.esh-catalog-label::after{background-image:url("../../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.esh-catalog-send{background-color:#83d01b;color:#fff;cursor:pointer;font-size:1rem;margin-top:-1.5rem;padding:.5rem;transition:all .35s}.esh-catalog-send:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-items{margin-top:1rem}.esh-catalog-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important}@media screen and (max-width:1024px){.esh-catalog-item{width:50%}}@media screen and (max-width:768px){.esh-catalog-item{width:100%}}.esh-catalog-thumbnail{max-width:370px;width:100%}.esh-catalog-button{background-color:#83d01b;border:0;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:80%}.esh-catalog-button.is-disabled{opacity:.5;pointer-events:none}.esh-catalog-button:hover{background-color:#4a760f;transition:all .35s}.esh-catalog-name{font-size:1rem;font-weight:300;margin-top:.5rem;text-align:center;text-transform:uppercase}.esh-catalog-price{font-size:28px;font-weight:900;text-align:center}.esh-catalog-price::before{content:'$'}

View File

@@ -0,0 +1,154 @@
@import '../variables';
.esh-catalog {
$banner-height: 260px;
&-hero {
background-image: url($image-main_banner);
background-size: cover;
height: $banner-height;
width: 100%;
}
&-title {
position: relative;
top: $banner-height / 3.5;
}
$filter-height: 65px;
&-filters {
background-color: $color-brand;
height: $filter-height;
}
$filter-padding: .5rem;
&-filter {
-webkit-appearance: none;
background-color: transparent;
border-color: $color-brand-bright;
color: $color-foreground-brighter;
cursor: pointer;
margin-right: 1rem;
margin-top: .5rem;
min-width: 140px;
outline-color: $color-secondary;
padding-bottom: 0;
padding-left: $filter-padding;
padding-right: $filter-padding;
padding-top: $filter-padding * 3;
option {
background-color: $color-brand;
}
}
&-label {
display: inline-block;
position: relative;
z-index: 0;
&::before {
color: rgba($color-foreground-brighter, .5);
content: attr(data-title);
font-size: $font-size-xs;
margin-left: $filter-padding;
margin-top: $font-size-xs;
position: absolute;
text-transform: uppercase;
z-index: 1;
}
&::after {
background-image: url($image-arrow_down);
content: '';
height: 7px; //png height
position: absolute;
right: $filter-padding * 3;
top: $filter-padding * 5;
width: 10px; //png width
z-index: 1;
}
}
&-send {
background-color: $color-secondary;
color: $color-foreground-brighter;
cursor: pointer;
font-size: $font-size-m;
margin-top: -$filter-padding * 3;
padding: $filter-padding;
transition: all $animation-speed-default;
&:hover {
background-color: $color-secondary-darker;
transition: all $animation-speed-default;
}
}
&-items {
margin-top: 1rem;
}
&-item {
margin-bottom: 1.5rem;
text-align: center;
width: 33%;
display: inline-block;
float: none !important;
@media screen and (max-width: $media-screen-m) {
width: 50%;
}
@media screen and (max-width: $media-screen-s) {
width: 100%;
}
}
&-thumbnail {
max-width: 370px;
width: 100%;
}
&-button {
background-color: $color-secondary;
border: 0;
color: $color-foreground-brighter;
cursor: pointer;
font-size: $font-size-m;
height: 3rem;
margin-top: 1rem;
transition: all $animation-speed-default;
width: 80%;
&.is-disabled {
opacity: .5;
pointer-events: none;
}
&:hover {
background-color: $color-secondary-darker;
transition: all $animation-speed-default;
}
}
&-name {
font-size: $font-size-m;
font-weight: $font-weight-semilight;
margin-top: .5rem;
text-align: center;
text-transform: uppercase;
}
&-price {
font-size: 28px;
font-weight: 900;
text-align: center;
&::before {
content: '$';
}
}
}

View File

@@ -0,0 +1,50 @@
.esh-orders {
min-height: 80vh;
overflow-x: hidden; }
.esh-orders-header {
background-color: #00A69C;
height: 4rem; }
.esh-orders-back {
color: rgba(255, 255, 255, 0.4);
line-height: 4rem;
text-decoration: none;
text-transform: uppercase;
transition: color 0.35s; }
.esh-orders-back:hover {
color: #FFFFFF;
transition: color 0.35s; }
.esh-orders-titles {
padding-bottom: 1rem;
padding-top: 2rem; }
.esh-orders-title {
text-transform: uppercase; }
.esh-orders-items {
height: 2rem;
line-height: 2rem;
position: relative; }
.esh-orders-items:nth-of-type(2n + 1):before {
background-color: #EEEEFF;
content: '';
height: 100%;
left: 0;
margin-left: -100vw;
position: absolute;
top: 0;
width: 200vw;
z-index: -1; }
.esh-orders-item {
font-weight: 300; }
.esh-orders-item--hover {
opacity: 0;
pointer-events: none; }
.esh-orders-items:hover .esh-orders-item--hover {
opacity: 1;
pointer-events: all; }
.esh-orders-link {
color: #83D01B;
text-decoration: none;
transition: color 0.35s; }
.esh-orders-link:hover {
color: #75b918;
transition: color 0.35s; }

View File

@@ -0,0 +1 @@
.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s}

View File

@@ -0,0 +1,91 @@
@import '../variables';
.esh-orders {
min-height: 80vh;
overflow-x: hidden;
$header-height: 4rem;
&-header
{
background-color: #00A69C;
height: $header-height;
}
&-back {
color: rgba($color-foreground-brighter, .4);
line-height: $header-height;
text-decoration: none;
text-transform: uppercase;
transition: color $animation-speed-default;
&:hover
{
color: $color-foreground-brighter;
transition: color $animation-speed-default;
}
}
&-titles {
padding-bottom: 1rem;
padding-top: 2rem;
}
&-title {
text-transform: uppercase;
}
&-items {
$height: 2rem;
height: $height;
line-height: $height;
position: relative;
&:nth-of-type(2n + 1)
{
&:before
{
background-color: $color-background-bright;
content: '';
height: 100%;
left: 0;
margin-left: -100vw;
position: absolute;
top: 0;
width: 200vw;
z-index: -1;
}
}
}
&-item {
font-weight: $font-weight-semilight;
&--hover
{
opacity: 0;
pointer-events: none;
}
}
&-items:hover &-item--hover {
opacity: 1;
pointer-events: all;
}
&-link {
color: $color-secondary;
text-decoration: none;
transition: color $animation-speed-default;
&:hover
{
color: $color-secondary-dark;
transition: color $animation-speed-default;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,14 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.0.0" />
</ItemGroup>

View File

@@ -1,47 +0,0 @@
using Infrastructure.FileSystem;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using Xunit;
using Moq;
namespace IntegrationTests.Infrastructure.File
{
public class LocalFileImageServiceGetImageBytesById
{
private byte[] _testBytes = new byte[] { 0x01, 0x02, 0x03 };
private readonly Mock<IHostingEnvironment> _mockEnvironment = new Mock<IHostingEnvironment>();
private int _testImageId = 123;
private string _testFileName = "123.png";
public LocalFileImageServiceGetImageBytesById()
{
// create folder if necessary
Directory.CreateDirectory(Path.Combine(GetFileDirectory(), "Pics"));
string filePath = GetFilePath(_testFileName);
System.IO.File.WriteAllBytes(filePath, _testBytes);
_mockEnvironment.SetupGet<string>(m => m.ContentRootPath).Returns(GetFileDirectory());
}
private string GetFilePath(string fileName)
{
return Path.Combine(GetFileDirectory(), "Pics", fileName);
}
private string GetFileDirectory()
{
var location = System.Reflection.Assembly.GetEntryAssembly().Location;
return Path.GetDirectoryName(location);
}
[Fact]
public void ReturnsFileContentResultGivenValidId()
{
var fileService = new LocalFileImageService(_mockEnvironment.Object);
var result = fileService.GetImageBytesById(_testImageId);
Assert.Equal(_testBytes, result);
}
}
}

View File

@@ -1,14 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="Moq" Version="4.7.49" />
<PackageReference Include="Moq" Version="4.7.99" />
</ItemGroup>

View File

@@ -1,15 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp1.1</TargetFramework>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<RuntimeFrameworkVersion>2.0.0</RuntimeFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
<PackageReference Include="Moq" Version="4.7.49" />
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.console" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />