diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index 35fa64fbd..e0c49c1de 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -1,7 +1,7 @@  - netstandard1.4 + netstandard2.0 diff --git a/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs new file mode 100644 index 000000000..072e12c9b --- /dev/null +++ b/src/ApplicationCore/Entities/BuyerAggregate/Buyer.cs @@ -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 _paymentMethods = new List(); + + public IEnumerable PaymentMethods => _paymentMethods.AsReadOnly(); + + protected Buyer() + { + } + + public Buyer(string identity) : this() + { + IdentityGuid = !string.IsNullOrWhiteSpace(identity) ? identity : throw new ArgumentNullException(nameof(identity)); + } + } +} diff --git a/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs new file mode 100644 index 000000000..e807e1cf7 --- /dev/null +++ b/src/ApplicationCore/Entities/BuyerAggregate/PaymentMethod.cs @@ -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; } + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/Address.cs b/src/ApplicationCore/Entities/OrderAggregate/Address.cs new file mode 100644 index 000000000..8f395e984 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/Address.cs @@ -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 GetAtomicValues() + //{ + // yield return Street; + // yield return City; + // yield return State; + // yield return Country; + // yield return ZipCode; + //} + + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs new file mode 100644 index 000000000..481c6b23d --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/CatalogItemOrdered.cs @@ -0,0 +1,23 @@ +namespace ApplicationCore.Entities.OrderAggregate +{ + /// + /// 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. + /// + 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; } + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/Order.cs b/src/ApplicationCore/Entities/OrderAggregate/Order.cs new file mode 100644 index 000000000..55a180759 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/Order.cs @@ -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 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 _orderItems = new List(); + + public IReadOnlyCollection 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; + } + + } +} diff --git a/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs new file mode 100644 index 000000000..d63824b33 --- /dev/null +++ b/src/ApplicationCore/Entities/OrderAggregate/OrderItem.cs @@ -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; + } + } +} diff --git a/src/ApplicationCore/Interfaces/IAggregateRoot.cs b/src/ApplicationCore/Interfaces/IAggregateRoot.cs new file mode 100644 index 000000000..84e3a1548 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IAggregateRoot.cs @@ -0,0 +1,5 @@ +namespace ApplicationCore.Interfaces +{ + public interface IAggregateRoot + { } +} diff --git a/src/ApplicationCore/Interfaces/IAppLogger.cs b/src/ApplicationCore/Interfaces/IAppLogger.cs index 2f3f0bc5d..53036cd89 100644 --- a/src/ApplicationCore/Interfaces/IAppLogger.cs +++ b/src/ApplicationCore/Interfaces/IAppLogger.cs @@ -6,6 +6,7 @@ /// public interface IAppLogger { + void LogInformation(string message, params object[] args); void LogWarning(string message, params object[] args); } } diff --git a/src/ApplicationCore/Interfaces/IAsyncRepository.cs b/src/ApplicationCore/Interfaces/IAsyncRepository.cs new file mode 100644 index 000000000..9bbcefdcf --- /dev/null +++ b/src/ApplicationCore/Interfaces/IAsyncRepository.cs @@ -0,0 +1,16 @@ +using Microsoft.eShopWeb.ApplicationCore.Entities; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + public interface IAsyncRepository where T : BaseEntity + { + Task GetByIdAsync(int id); + Task> ListAllAsync(); + Task> ListAsync(ISpecification spec); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); + } +} diff --git a/src/ApplicationCore/Interfaces/IImageService.cs b/src/ApplicationCore/Interfaces/IImageService.cs deleted file mode 100644 index d2d01c798..000000000 --- a/src/ApplicationCore/Interfaces/IImageService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace ApplicationCore.Interfaces -{ - public interface IImageService - { - byte[] GetImageBytesById(int id); - } -} diff --git a/src/ApplicationCore/Interfaces/IOrderRepository.cs b/src/ApplicationCore/Interfaces/IOrderRepository.cs new file mode 100644 index 000000000..8eb971346 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IOrderRepository.cs @@ -0,0 +1,12 @@ +using ApplicationCore.Entities.OrderAggregate; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + + public interface IOrderRepository : IRepository, IAsyncRepository + { + Order GetByIdWithItems(int id); + Task GetByIdWithItemsAsync(int id); + } +} diff --git a/src/ApplicationCore/Interfaces/IOrderService.cs b/src/ApplicationCore/Interfaces/IOrderService.cs new file mode 100644 index 000000000..3c37ab146 --- /dev/null +++ b/src/ApplicationCore/Interfaces/IOrderService.cs @@ -0,0 +1,10 @@ +using ApplicationCore.Entities.OrderAggregate; +using System.Threading.Tasks; + +namespace ApplicationCore.Interfaces +{ + public interface IOrderService + { + Task CreateOrderAsync(int basketId, Address shippingAddress); + } +} diff --git a/src/ApplicationCore/Interfaces/IRepository.cs b/src/ApplicationCore/Interfaces/IRepository.cs index a72abad4b..a448152fa 100644 --- a/src/ApplicationCore/Interfaces/IRepository.cs +++ b/src/ApplicationCore/Interfaces/IRepository.cs @@ -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 where T : BaseEntity { T GetById(int id); - List List(); - List List(ISpecification spec); + IEnumerable ListAll(); + IEnumerable List(ISpecification spec); T Add(T entity); void Update(T entity); void Delete(T entity); diff --git a/src/ApplicationCore/Interfaces/ISpecification.cs b/src/ApplicationCore/Interfaces/ISpecification.cs index 6cff09602..5b1173486 100644 --- a/src/ApplicationCore/Interfaces/ISpecification.cs +++ b/src/ApplicationCore/Interfaces/ISpecification.cs @@ -8,6 +8,7 @@ public interface ISpecification { Expression> Criteria { get; } List>> Includes { get; } + List IncludeStrings { get; } void AddInclude(Expression> includeExpression); } } diff --git a/src/ApplicationCore/Interfaces/IUriComposer.cs b/src/ApplicationCore/Interfaces/IUriComposer.cs index 2e4c4eaea..ccd78127a 100644 --- a/src/ApplicationCore/Interfaces/IUriComposer.cs +++ b/src/ApplicationCore/Interfaces/IUriComposer.cs @@ -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); diff --git a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs index a70c36ecb..a44fb7766 100644 --- a/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs +++ b/src/ApplicationCore/Specifications/BasketWithItemsSpecification.cs @@ -3,6 +3,7 @@ using System; using System.Linq.Expressions; using System.Collections.Generic; +using ApplicationCore.Entities.OrderAggregate; namespace ApplicationCore.Specifications { @@ -28,6 +29,8 @@ public BasketWithItemsSpecification(string buyerId) public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + public void AddInclude(Expression> includeExpression) { Includes.Add(includeExpression); diff --git a/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs index 30d414eba..54e1f8628 100644 --- a/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs +++ b/src/ApplicationCore/Specifications/CatalogFilterSpecification.cs @@ -24,6 +24,8 @@ public CatalogFilterSpecification(int? brandId, int? typeId) public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + public void AddInclude(Expression> includeExpression) { Includes.Add(includeExpression); diff --git a/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs b/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs new file mode 100644 index 000000000..abfa9b658 --- /dev/null +++ b/src/ApplicationCore/Specifications/CustomerOrdersWithItemsSpecification.cs @@ -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 + { + private readonly string _buyerId; + + public CustomerOrdersWithItemsSpecification(string buyerId) + { + _buyerId = buyerId; + AddInclude(o => o.OrderItems); + AddInclude("OrderItems.ItemOrdered"); + } + + public Expression> Criteria => o => o.BuyerId == _buyerId; + + public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + + public void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + + public void AddInclude(string includeString) + { + IncludeStrings.Add(includeString); + } + } +} diff --git a/src/Infrastructure/Data/CatalogContext.cs b/src/Infrastructure/Data/CatalogContext.cs index da13128ef..ec3370883 100644 --- a/src/Infrastructure/Data/CatalogContext.cs +++ b/src/Infrastructure/Data/CatalogContext.cs @@ -3,6 +3,7 @@ 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 @@ public CatalogContext(DbContextOptions options) : base(options) public DbSet CatalogItems { get; set; } public DbSet CatalogBrands { get; set; } public DbSet CatalogTypes { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderItems { get; set; } protected override void OnModelCreating(ModelBuilder builder) { @@ -27,6 +30,8 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity(ConfigureCatalogBrand); builder.Entity(ConfigureCatalogType); builder.Entity(ConfigureCatalogItem); + builder.Entity(ConfigureOrder); + builder.Entity(ConfigureOrderItem); } private void ConfigureBasket(EntityTypeBuilder builder) @@ -36,7 +41,7 @@ private void ConfigureBasket(EntityTypeBuilder builder) navigation.SetPropertyAccessMode(PropertyAccessMode.Field); } - void ConfigureCatalogItem(EntityTypeBuilder builder) + private void ConfigureCatalogItem(EntityTypeBuilder builder) { builder.ToTable("Catalog"); @@ -63,7 +68,7 @@ void ConfigureCatalogItem(EntityTypeBuilder builder) .HasForeignKey(ci => ci.CatalogTypeId); } - void ConfigureCatalogBrand(EntityTypeBuilder builder) + private void ConfigureCatalogBrand(EntityTypeBuilder builder) { builder.ToTable("CatalogBrand"); @@ -78,7 +83,7 @@ void ConfigureCatalogBrand(EntityTypeBuilder builder) .HasMaxLength(100); } - void ConfigureCatalogType(EntityTypeBuilder builder) + private void ConfigureCatalogType(EntityTypeBuilder builder) { builder.ToTable("CatalogType"); @@ -92,5 +97,14 @@ void ConfigureCatalogType(EntityTypeBuilder builder) .IsRequired() .HasMaxLength(100); } + private void ConfigureOrder(EntityTypeBuilder builder) + { + builder.OwnsOne(o => o.ShipToAddress); + } + private void ConfigureOrderItem(EntityTypeBuilder builder) + { + builder.OwnsOne(i => i.ItemOrdered); + } + } } diff --git a/src/Infrastructure/Data/CatalogContextSeed.cs b/src/Infrastructure/Data/CatalogContextSeed.cs index a7c1845db..45536840b 100644 --- a/src/Infrastructure/Data/CatalogContextSeed.cs +++ b/src/Infrastructure/Data/CatalogContextSeed.cs @@ -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 @@ public static async Task SeedAsync(IApplicationBuilder applicationBuilder, ILogg if (retryForAvailability < 10) { retryForAvailability++; - var log = loggerFactory.CreateLogger("catalog seed"); + var log = loggerFactory.CreateLogger(); log.LogError(ex.Message); - await SeedAsync(applicationBuilder, loggerFactory, retryForAvailability); + await SeedAsync(applicationBuilder, catalogContext, loggerFactory, retryForAvailability); } } } diff --git a/src/Infrastructure/Data/EfRepository.cs b/src/Infrastructure/Data/EfRepository.cs index 9c7282f3d..cdc84fa44 100644 --- a/src/Infrastructure/Data/EfRepository.cs +++ b/src/Infrastructure/Data/EfRepository.cs @@ -3,36 +3,68 @@ using Microsoft.eShopWeb.ApplicationCore.Entities; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; namespace Infrastructure.Data { - public class EfRepository : IRepository where T : BaseEntity + /// + /// "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/ + /// + /// + public class EfRepository : IRepository, IAsyncRepository 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().SingleOrDefault(e => e.Id == id); + return _dbContext.Set().Find(id); } - public List List() + public virtual async Task GetByIdAsync(int id) { - return _dbContext.Set().ToList(); + return await _dbContext.Set().FindAsync(id); } - public List List(ISpecification spec) + public IEnumerable ListAll() + { + return _dbContext.Set().AsEnumerable(); + } + + public async Task> ListAllAsync() + { + return await _dbContext.Set().ToListAsync(); + } + + public IEnumerable List(ISpecification spec) + { + var queryableResultWithIncludes = spec.Includes + .Aggregate(_dbContext.Set().AsQueryable(), + (current, include) => current.Include(include)); + var secondaryResult = spec.IncludeStrings + .Aggregate(queryableResultWithIncludes, + (current, include) => current.Include(include)); + return secondaryResult + .Where(spec.Criteria) + .AsEnumerable(); + } + public async Task> ListAsync(ISpecification spec) { var queryableResultWithIncludes = spec.Includes - .Aggregate(_dbContext.Set().AsQueryable(), - (current, include) => current.Include(include)); - return queryableResultWithIncludes + .Aggregate(_dbContext.Set().AsQueryable(), + (current, include) => current.Include(include)); + var secondaryResult = spec.IncludeStrings + .Aggregate(queryableResultWithIncludes, + (current, include) => current.Include(include)); + + return await secondaryResult .Where(spec.Criteria) - .ToList(); + .ToListAsync(); } public T Add(T entity) @@ -43,10 +75,12 @@ public T Add(T entity) return entity; } - public void Delete(T entity) + public async Task AddAsync(T entity) { - _dbContext.Set().Remove(entity); - _dbContext.SaveChanges(); + _dbContext.Set().Add(entity); + await _dbContext.SaveChangesAsync(); + + return entity; } public void Update(T entity) @@ -54,6 +88,21 @@ public void Update(T entity) _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().Remove(entity); + _dbContext.SaveChanges(); + } + public async Task DeleteAsync(T entity) + { + _dbContext.Set().Remove(entity); + await _dbContext.SaveChangesAsync(); + } } } diff --git a/src/Infrastructure/Data/OrderRepository.cs b/src/Infrastructure/Data/OrderRepository.cs new file mode 100644 index 000000000..35c54650d --- /dev/null +++ b/src/Infrastructure/Data/OrderRepository.cs @@ -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, 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 GetByIdWithItemsAsync(int id) + { + return _dbContext.Orders + .Include(o => o.OrderItems) + .Include("OrderItems.ItemOrdered") + .FirstOrDefaultAsync(); + } + } +} diff --git a/src/Infrastructure/FileSystem/LocalFileImageService.cs b/src/Infrastructure/FileSystem/LocalFileImageService.cs deleted file mode 100644 index 82db8b297..000000000 --- a/src/Infrastructure/FileSystem/LocalFileImageService.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/src/Infrastructure/Identity/ApplicationUser.cs b/src/Infrastructure/Identity/ApplicationUser.cs index a8099f375..99ee879f9 100644 --- a/src/Infrastructure/Identity/ApplicationUser.cs +++ b/src/Infrastructure/Identity/ApplicationUser.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; namespace Infrastructure.Identity diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 06ff46f02..4497c8389 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -1,30 +1,26 @@  - netstandard1.4 + netcoreapp2.0 + + + + 2.0.0 - - - - - - - - - + + - - - + + + - \ No newline at end of file diff --git a/src/Infrastructure/Logging/LoggerAdapter.cs b/src/Infrastructure/Logging/LoggerAdapter.cs index b79e8433d..93b544769 100644 --- a/src/Infrastructure/Logging/LoggerAdapter.cs +++ b/src/Infrastructure/Logging/LoggerAdapter.cs @@ -10,9 +10,14 @@ public LoggerAdapter(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); } + public void LogWarning(string message, params object[] args) { _logger.LogWarning(message, args); } + public void LogInformation(string message, params object[] args) + { + _logger.LogInformation(message, args); + } } } diff --git a/src/Infrastructure/Services/OrderService.cs b/src/Infrastructure/Services/OrderService.cs new file mode 100644 index 000000000..d7762858f --- /dev/null +++ b/src/Infrastructure/Services/OrderService.cs @@ -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 _orderRepository; + private readonly IAsyncRepository _basketRepository; + private readonly IAsyncRepository _itemRepository; + + public OrderService(IAsyncRepository basketRepository, + IAsyncRepository itemRepository, + IAsyncRepository 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(); + 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); + } + } +} diff --git a/src/Web/Controllers/AccountController.cs b/src/Web/Controllers/AccountController.cs index 162397151..b78e110b7 100644 --- a/src/Web/Controllers/AccountController.cs +++ b/src/Web/Controllers/AccountController.cs @@ -3,12 +3,11 @@ 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 @@ public class AccountController : Controller { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly string _externalCookieScheme; private readonly IBasketService _basketService; public AccountController( UserManager userManager, SignInManager signInManager, - IOptions identityCookieOptions, IBasketService basketService) { _userManager = userManager; _signInManager = signInManager; - _externalCookieScheme = identityCookieOptions.Value.ExternalCookieAuthenticationScheme; _basketService = basketService; } @@ -37,9 +33,15 @@ public AccountController( [AllowAnonymous] public async Task 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 @@ public async Task SignIn(LoginViewModel model, string returnUrl = 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 @@ public async Task SignIn(LoginViewModel model, string returnUrl = [ValidateAntiForgeryToken] public async Task SignOut() { - HttpContext.Session.Clear(); await _signInManager.SignOutAsync(); return RedirectToAction(nameof(CatalogController.Index), "Catalog"); diff --git a/src/Web/Controllers/BasketController.cs b/src/Web/Controllers/BasketController.cs index 3aea615fd..a1ce98093 100644 --- a/src/Web/Controllers/BasketController.cs +++ b/src/Web/Controllers/BasketController.cs @@ -7,6 +7,9 @@ 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 @@ public class BasketController : Controller private const string _basketSessionKey = "basketId"; private readonly IUriComposer _uriComposer; private readonly SignInManager _signInManager; + private readonly IAppLogger _logger; + private readonly IOrderService _orderService; public BasketController(IBasketService basketService, + IOrderService orderService, IUriComposer uriComposer, - SignInManager signInManager) + SignInManager signInManager, + IAppLogger logger) { _basketService = basketService; _uriComposer = uriComposer; _signInManager = signInManager; + _logger = logger; + _orderService = orderService; } [HttpGet] @@ -35,6 +44,16 @@ public async Task Index() return View(basketModel); } + [HttpPost] + public async Task Index(Dictionary items) + { + var basketViewModel = await GetBasketViewModelAsync(); + await _basketService.SetQuantities(basketViewModel.Id, items); + + return View(await GetBasketViewModelAsync()); + } + + // POST: /Basket/AddToBasket [HttpPost] public async Task AddToBasket(CatalogItemViewModel productDetails) @@ -51,11 +70,15 @@ public async Task AddToBasket(CatalogItemViewModel productDetails } [HttpPost] - public async Task Checkout() + [Authorize] + public async Task Checkout(Dictionary items) { - var basket = await GetBasketViewModelAsync(); + var basketViewModel = await GetBasketViewModelAsync(); + await _basketService.SetQuantities(basketViewModel.Id, items); + + await _orderService.CreateOrderAsync(basketViewModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240")); - await _basketService.Checkout(basket.Id); + await _basketService.DeleteBasketAsync(basketViewModel.Id); return View("Checkout"); } diff --git a/src/Web/Controllers/OrderController.cs b/src/Web/Controllers/OrderController.cs new file mode 100644 index 000000000..65ad091bd --- /dev/null +++ b/src/Web/Controllers/OrderController.cs @@ -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 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 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; + } + } +} diff --git a/src/Web/Interfaces/IBasketService.cs b/src/Web/Interfaces/IBasketService.cs index 7794e7c4b..c76e42a34 100644 --- a/src/Web/Interfaces/IBasketService.cs +++ b/src/Web/Interfaces/IBasketService.cs @@ -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 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 quantities); + Task DeleteBasketAsync(int basketId); } } diff --git a/src/Web/Program.cs b/src/Web/Program.cs index b8c01c5b8..20289becb 100644 --- a/src/Web/Program.cs +++ b/src/Web/Program.cs @@ -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() + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) .UseUrls("http://0.0.0.0:5106") - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureLogging(factory => - { - factory.AddConsole(LogLevel.Warning); - factory.AddDebug(); - }) - .UseIISIntegration() .UseStartup() - .UseApplicationInsights() .Build(); - - host.Run(); - } } } diff --git a/src/Web/Services/BasketService.cs b/src/Web/Services/BasketService.cs index 5e74e2273..206598760 100644 --- a/src/Web/Services/BasketService.cs +++ b/src/Web/Services/BasketService.cs @@ -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 _basketRepository; + private readonly IAsyncRepository _basketRepository; private readonly IUriComposer _uriComposer; + private readonly IAppLogger _logger; private readonly IRepository _itemRepository; - public BasketService(IRepository basketRepository, + public BasketService(IAsyncRepository basketRepository, IRepository itemRepository, - IUriComposer uriComposer) + IUriComposer uriComposer, + IAppLogger logger) { _basketRepository = basketRepository; _uriComposer = uriComposer; + this._logger = logger; _itemRepository = itemRepository; } public async Task 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 @@ private BasketViewModel CreateViewModelFromBasket(Basket basket) public async Task CreateBasketForUser(string userId) { var basket = new Basket() { BuyerId = userId }; - _basketRepository.Add(basket); + await _basketRepository.AddAsync(basket); return new BasketViewModel() { @@ -75,30 +77,41 @@ public async Task CreateBasketForUser(string userId) 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 quantities) { - var basket = _basketRepository.GetById(basketId); - - // TODO: Actually Process the order + 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); + } - _basketRepository.Delete(basket); + public async Task DeleteBasketAsync(int basketId) + { + var basket = await _basketRepository.GetByIdAsync(basketId); + + await _basketRepository.DeleteAsync(basket); } - public Task TransferBasket(string anonymousId, string userName) + 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); } } } diff --git a/src/Web/Services/CatalogService.cs b/src/Web/Services/CatalogService.cs index 1c1446f5c..8049b1fd6 100644 --- a/src/Web/Services/CatalogService.cs +++ b/src/Web/Services/CatalogService.cs @@ -15,15 +15,15 @@ public class CatalogService : ICatalogService { private readonly ILogger _logger; private readonly IRepository _itemRepository; - private readonly IRepository _brandRepository; - private readonly IRepository _typeRepository; + private readonly IAsyncRepository _brandRepository; + private readonly IAsyncRepository _typeRepository; private readonly IUriComposer _uriComposer; public CatalogService( ILoggerFactory loggerFactory, IRepository itemRepository, - IRepository brandRepository, - IRepository typeRepository, + IAsyncRepository brandRepository, + IAsyncRepository typeRepository, IUriComposer uriComposer) { _logger = loggerFactory.CreateLogger(); @@ -83,7 +83,7 @@ public async Task GetCatalogItems(int pageIndex, int item public async Task> GetBrands() { _logger.LogInformation("GetBrands called."); - var brands = _brandRepository.List(); + var brands = await _brandRepository.ListAllAsync(); var items = new List { @@ -100,7 +100,7 @@ public async Task> GetBrands() public async Task> GetTypes() { _logger.LogInformation("GetTypes called."); - var types = _typeRepository.List(); + var types = await _typeRepository.ListAllAsync(); var items = new List { new SelectListItem() { Value = null, Text = "All", Selected = true } diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index 31de781e2..87c90def4 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -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 @@ public void ConfigureServices(IServiceCollection services) { 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 @@ public void ConfigureServices(IServiceCollection services) .AddEntityFrameworkStores() .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(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.Configure(Configuration); services.AddSingleton(new UriComposer(Configuration.Get())); - // TODO: Remove - services.AddSingleton(); - services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>)); - // Add memory cache services services.AddMemoryCache(); @@ -98,25 +94,8 @@ public void Configure(IApplicationBuilder app, { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); - - app.Map("/allservices", builder => builder.Run(async context => - { - var sb = new StringBuilder(); - sb.Append("

All Services

"); - sb.Append(""); - sb.Append(""); - sb.Append(""); - foreach (var svc in _services) - { - sb.Append(""); - sb.Append($""); - sb.Append($""); - sb.Append($""); - sb.Append(""); - } - sb.Append("
TypeLifetimeInstance
{svc.ServiceType.FullName}{svc.Lifetime}{svc.ImplementationType?.FullName}
"); - await context.Response.WriteAsync(sb.ToString()); - })); + ListAllRegisteredServices(app); + app.UseDatabaseErrorPage(); } else { @@ -124,20 +103,43 @@ public void Configure(IApplicationBuilder app, } app.UseStaticFiles(); - app.UseIdentity(); + app.UseAuthentication(); app.UseMvc(); } + private void ListAllRegisteredServices(IApplicationBuilder app) + { + app.Map("/allservices", builder => builder.Run(async context => + { + var sb = new StringBuilder(); + sb.Append("

All Services

"); + sb.Append(""); + sb.Append(""); + sb.Append(""); + foreach (var svc in _services) + { + sb.Append(""); + sb.Append($""); + sb.Append($""); + sb.Append($""); + sb.Append(""); + } + sb.Append("
TypeLifetimeInstance
{svc.ServiceType.FullName}{svc.Lifetime}{svc.ImplementationType?.FullName}
"); + await context.Response.WriteAsync(sb.ToString()); + })); + } + public void ConfigureDevelopment(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, - UserManager userManager) + UserManager 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 @@ public void ConfigureDevelopment(IApplicationBuilder app, public void ConfigureProduction(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, - UserManager userManager) + UserManager 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(); - } } } diff --git a/src/Web/ViewComponents/Basket.cs b/src/Web/ViewComponents/Basket.cs new file mode 100644 index 000000000..47effdfca --- /dev/null +++ b/src/Web/ViewComponents/Basket.cs @@ -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 _signInManager; + + public Basket(IBasketService basketService, + SignInManager signInManager) + { + _basketService = basketService; + _signInManager = signInManager; + } + + public async Task InvokeAsync(string userName) + { + var vm = new BasketComponentViewModel(); + vm.ItemsCount = (await GetBasketViewModelAsync()).Items.Sum(i => i.Quantity); + return View(vm); + } + + private async Task 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; + } + } +} diff --git a/src/Web/ViewModels/BasketComponentViewModel.cs b/src/Web/ViewModels/BasketComponentViewModel.cs new file mode 100644 index 000000000..56725a5bb --- /dev/null +++ b/src/Web/ViewModels/BasketComponentViewModel.cs @@ -0,0 +1,7 @@ +namespace Microsoft.eShopWeb.ViewModels +{ + public class BasketComponentViewModel + { + public int ItemsCount { get; set; } + } +} diff --git a/src/Web/ViewModels/OrderItemViewModel.cs b/src/Web/ViewModels/OrderItemViewModel.cs new file mode 100644 index 000000000..84819da91 --- /dev/null +++ b/src/Web/ViewModels/OrderItemViewModel.cs @@ -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; } + } + +} diff --git a/src/Web/ViewModels/OrderViewModel.cs b/src/Web/ViewModels/OrderViewModel.cs new file mode 100644 index 000000000..caebd890c --- /dev/null +++ b/src/Web/ViewModels/OrderViewModel.cs @@ -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 OrderItems { get; set; } = new List(); + + } + +} diff --git a/src/Web/ViewModels/RegisterViewModel.cs b/src/Web/ViewModels/RegisterViewModel.cs index 172446144..be6e2c362 100644 --- a/src/Web/ViewModels/RegisterViewModel.cs +++ b/src/Web/ViewModels/RegisterViewModel.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Microsoft.eShopWeb.ViewModels { diff --git a/src/Web/Views/Basket/Index.cshtml b/src/Web/Views/Basket/Index.cshtml index a68b37e1b..4915da915 100644 --- a/src/Web/Views/Basket/Index.cshtml +++ b/src/Web/Views/Basket/Index.cshtml @@ -1,7 +1,7 @@ @using Microsoft.eShopWeb.ViewModels +@model BasketViewModel @{ ViewData["Title"] = "Basket"; - @model BasketViewModel }
@@ -23,9 +23,10 @@
Cost
- @foreach (var item in Model.Items) + @for (int i=0; i< Model.Items.Count; i++) { -
+ var item = Model.Items[i]; +
@@ -33,8 +34,8 @@
@item.ProductName
$ @item.UnitPrice.ToString("N2")
- - + +
$ @Math.Round(item.Quantity * item.UnitPrice, 2).ToString("N2")
@@ -65,7 +66,9 @@
} -
+
+ diff --git a/src/Web/Views/Order/Detail.cshtml b/src/Web/Views/Order/Detail.cshtml new file mode 100644 index 000000000..9cc765681 --- /dev/null +++ b/src/Web/Views/Order/Detail.cshtml @@ -0,0 +1,88 @@ +@using Microsoft.eShopWeb.ViewModels +@model OrderViewModel +@{ + ViewData["Title"] = "My Order History"; +} +@{ + ViewData["Title"] = "Order Detail"; +} + +
+
+
+
+
Order number
+
Date
+
Total
+
Status
+
+ +
+
@Model.OrderNumber
+
@Model.OrderDate
+
$@Model.Total
+
@Model.Status
+
+
+ + @*
+
+
Description
+
+ +
+
@Model.Description
+
+
*@ + +
+
+
Shipping Address
+
+ +
+
@Model.ShippingAddress.Street
+
+ +
+
@Model.ShippingAddress.City
+
+ +
+
@Model.ShippingAddress.Country
+
+
+ +
+
+
ORDER DETAILS
+
+ + @for (int i = 0; i < Model.OrderItems.Count; i++) + { + var item = Model.OrderItems[i]; +
+
+ +
+
@item.ProductName
+
$ @item.UnitPrice.ToString("N2")
+
@item.Units
+
$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")
+
+ } +
+ +
+
+
+
TOTAL
+
+ +
+
+
$ @Model.Total
+
+
+
+
diff --git a/src/Web/Views/Order/Index.cshtml b/src/Web/Views/Order/Index.cshtml new file mode 100644 index 000000000..838ff58fd --- /dev/null +++ b/src/Web/Views/Order/Index.cshtml @@ -0,0 +1,39 @@ +@using Microsoft.eShopWeb.ViewModels +@model IEnumerable +@{ + ViewData["Title"] = "My Order History"; +} + +
+
+

@ViewData["Title"]

+
+
Order number
+
Date
+
Total
+
Status
+
+
+ @if (Model != null && Model.Any()) + { + @foreach (var item in Model) + { +
+
@Html.DisplayFor(modelItem => item.OrderNumber)
+
@Html.DisplayFor(modelItem => item.OrderDate)
+
$ @Html.DisplayFor(modelItem => item.Total)
+
@Html.DisplayFor(modelItem => item.Status)
+
+ Detail +
+
+ @if (item.Status.ToLower() == "submitted") + { + Cancel + } +
+
+ } + } +
+
diff --git a/src/Web/Views/Shared/Components/Basket/Default.cshtml b/src/Web/Views/Shared/Components/Basket/Default.cshtml new file mode 100644 index 000000000..56d4fd05d --- /dev/null +++ b/src/Web/Views/Shared/Components/Basket/Default.cshtml @@ -0,0 +1,17 @@ +@model BasketComponentViewModel + +@{ + ViewData["Title"] = "My Basket"; +} + + +
+ +
+
+ @Model.ItemsCount +
+
diff --git a/src/Web/Views/Shared/_Layout.cshtml b/src/Web/Views/Shared/_Layout.cshtml index a8cb740eb..998deb2c4 100644 --- a/src/Web/Views/Shared/_Layout.cshtml +++ b/src/Web/Views/Shared/_Layout.cshtml @@ -15,8 +15,12 @@ asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> - - + + + + + +
- @*
- @await Component.InvokeAsync("Basket", new { user = UserManager.Parse(User) }) -
*@ +
+ @await Component.InvokeAsync("Basket", User.Identity.Name) +
} else @@ -53,5 +52,7 @@ else - @*
*@ +
+ @await Component.InvokeAsync("Basket") +
} diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index 937c90bf6..c8cc466c8 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -1,41 +1,21 @@  - netcoreapp1.1 + netcoreapp2.0 - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; - 1.1.0 - ..\docker-compose.dcproj + 2.0.0 - - - - - - - - - - - - - - - - - - - - + + + - @@ -43,9 +23,9 @@ - - - + + + diff --git a/src/Web/compilerconfig.json b/src/Web/compilerconfig.json new file mode 100644 index 000000000..73e8abe11 --- /dev/null +++ b/src/Web/compilerconfig.json @@ -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" + } +] \ No newline at end of file diff --git a/src/Web/wwwroot/css/_variables.scss b/src/Web/wwwroot/css/_variables.scss new file mode 100644 index 000000000..4b989c185 --- /dev/null +++ b/src/Web/wwwroot/css/_variables.scss @@ -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'; \ No newline at end of file diff --git a/src/Web/wwwroot/css/app.component.css b/src/Web/wwwroot/css/app.component.css new file mode 100644 index 000000000..49213508e --- /dev/null +++ b/src/Web/wwwroot/css/app.component.css @@ -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; } + diff --git a/src/Web/wwwroot/css/app.component.min.css b/src/Web/wwwroot/css/app.component.min.css new file mode 100644 index 000000000..b4c963577 --- /dev/null +++ b/src/Web/wwwroot/css/app.component.min.css @@ -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} \ No newline at end of file diff --git a/src/Web/wwwroot/css/app.component.scss b/src/Web/wwwroot/css/app.component.scss new file mode 100644 index 000000000..e823b6f32 --- /dev/null +++ b/src/Web/wwwroot/css/app.component.scss @@ -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; + } + + } +} \ No newline at end of file diff --git a/src/Web/wwwroot/css/basket/basket-status/basket-status.component.css b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.css new file mode 100644 index 000000000..8597d36db --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.css @@ -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; } + diff --git a/src/Web/wwwroot/css/basket/basket-status/basket-status.component.min.css b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.min.css new file mode 100644 index 000000000..0b3f1a97e --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.min.css @@ -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} \ No newline at end of file diff --git a/src/Web/wwwroot/css/basket/basket-status/basket-status.component.scss b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.scss new file mode 100644 index 000000000..5dd28de99 --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket-status/basket-status.component.scss @@ -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; + } +} diff --git a/src/Web/wwwroot/css/basket/basket.component.css b/src/Web/wwwroot/css/basket/basket.component.css new file mode 100644 index 000000000..77d3f88d5 --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket.component.css @@ -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; } + diff --git a/src/Web/wwwroot/css/basket/basket.component.min.css b/src/Web/wwwroot/css/basket/basket.component.min.css new file mode 100644 index 000000000..5ed080aef --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket.component.min.css @@ -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} \ No newline at end of file diff --git a/src/Web/wwwroot/css/basket/basket.component.scss b/src/Web/wwwroot/css/basket/basket.component.scss new file mode 100644 index 000000000..c0e6c61fd --- /dev/null +++ b/src/Web/wwwroot/css/basket/basket.component.scss @@ -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; + } + } +} + + + diff --git a/src/Web/wwwroot/css/catalog/catalog.component.css b/src/Web/wwwroot/css/catalog/catalog.component.css index ac401e238..3517e061e 100644 --- a/src/Web/wwwroot/css/catalog/catalog.component.css +++ b/src/Web/wwwroot/css/catalog/catalog.component.css @@ -1,228 +1,117 @@ -.esh-catalog-hero { - background-image: url("../../images/main_banner.png"); - background-size: cover; - height: 260px; - width: 100%; -} +.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; -} + position: relative; + top: 74.28571px; } .esh-catalog-filters { - background-color: #00A69C; - height: 65px; -} + background-color: #00A69C; + height: 65px; } .esh-catalog-filter { - background-color: transparent; - border-color: #00d9cc; - color: #FFFFFF; - cursor: pointer; - margin-right: 1rem; - margin-top: .5rem; - outline-color: #83D01B; - padding-bottom: 0; - padding-left: 0.5rem; - padding-right: 0.5rem; - padding-top: 1.5rem; - min-width: 140px; - -webkit-appearance: none; -} - - .esh-catalog-filter option { - background-color: #00A69C; - } + -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; } + .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, 0.5); - content: attr(data-title); - font-size: 0.65rem; - margin-top: 0.65rem; - margin-left: 0.5rem; - position: absolute; - text-transform: uppercase; - z-index: 1; - } - - .esh-catalog-label::after { - background-image: url("../../images/arrow-down.png"); - height: 7px; - content: ''; - position: absolute; - right: 1.5rem; - top: 2.5rem; - width: 10px; - z-index: 1; - } + display: inline-block; + position: relative; + z-index: 0; } + .esh-catalog-label::before { + color: rgba(255, 255, 255, 0.5); + content: attr(data-title); + font-size: 0.65rem; + margin-left: 0.5rem; + margin-top: 0.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: #FFFFFF; - cursor: pointer; - font-size: 1rem; - transform: translateY(.5rem); - padding: 0.5rem; - transition: all 0.35s; -} - - .esh-catalog-send:hover { - background-color: #4a760f; - transition: all 0.35s; - } + background-color: #83D01B; + color: #FFFFFF; + cursor: pointer; + font-size: 1rem; + margin-top: -1.5rem; + padding: 0.5rem; + transition: all 0.35s; } + .esh-catalog-send:hover { + background-color: #4a760f; + transition: all 0.35s; } .esh-catalog-items { - margin-top: 1rem; -} + margin-top: 1rem; } .esh-catalog-item { - text-align: center; - margin-bottom: 1.5rem; - width: 33%; - display: inline-block; - float: none !important; -} - -@media screen and (max-width: 1024px) { + 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) { + width: 50%; } } + @media screen and (max-width: 768px) { .esh-catalog-item { - width: 100%; - } -} + width: 100%; } } .esh-catalog-thumbnail { - max-width: 370px; - width: 100%; -} + max-width: 370px; + width: 100%; } .esh-catalog-button { - background-color: #83D01B; - border: none; - color: #FFFFFF; - cursor: pointer; - font-size: 1rem; - height: 3rem; - margin-top: 1rem; - transition: all 0.35s; - width: 80%; -} - .esh-catalog-button.is-disabled { - opacity: .5; - pointer-events: none; - } - - .esh-catalog-button:hover { - background-color: #4a760f; - transition: all 0.35s; - } + background-color: #83D01B; + border: 0; + color: #FFFFFF; + cursor: pointer; + font-size: 1rem; + height: 3rem; + margin-top: 1rem; + transition: all 0.35s; + width: 80%; } + .esh-catalog-button.is-disabled { + opacity: .5; + pointer-events: none; } + .esh-catalog-button:hover { + background-color: #4a760f; + transition: all 0.35s; } .esh-catalog-name { - font-size: 1rem; - font-weight: 300; - margin-top: .5rem; - text-align: center; - text-transform: uppercase; -} + font-size: 1rem; + font-weight: 300; + margin-top: .5rem; + text-align: center; + text-transform: uppercase; } .esh-catalog-price { - text-align: center; - font-weight: 900; - font-size: 28px; -} - - .esh-catalog-price::before { - 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; -} + font-size: 28px; + font-weight: 900; + text-align: center; } + .esh-catalog-price::before { + content: '$'; } - .esh-basket-checkout:hover { - background-color: #4a760f; - transition: all 0.35s; - } \ No newline at end of file diff --git a/src/Web/wwwroot/css/catalog/catalog.component.min.css b/src/Web/wwwroot/css/catalog/catalog.component.min.css new file mode 100644 index 000000000..76a8798f3 --- /dev/null +++ b/src/Web/wwwroot/css/catalog/catalog.component.min.css @@ -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:'$'} \ No newline at end of file diff --git a/src/Web/wwwroot/css/catalog/catalog.component.scss b/src/Web/wwwroot/css/catalog/catalog.component.scss new file mode 100644 index 000000000..56291fdd2 --- /dev/null +++ b/src/Web/wwwroot/css/catalog/catalog.component.scss @@ -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: '$'; + } + } +} diff --git a/src/Web/wwwroot/css/orders/orders.component.css b/src/Web/wwwroot/css/orders/orders.component.css new file mode 100644 index 000000000..49aaff00d --- /dev/null +++ b/src/Web/wwwroot/css/orders/orders.component.css @@ -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; } + diff --git a/src/Web/wwwroot/css/orders/orders.component.min.css b/src/Web/wwwroot/css/orders/orders.component.min.css new file mode 100644 index 000000000..03762a0a5 --- /dev/null +++ b/src/Web/wwwroot/css/orders/orders.component.min.css @@ -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} \ No newline at end of file diff --git a/src/Web/wwwroot/css/orders/orders.component.scss b/src/Web/wwwroot/css/orders/orders.component.scss new file mode 100644 index 000000000..1371a7732 --- /dev/null +++ b/src/Web/wwwroot/css/orders/orders.component.scss @@ -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; +} + +} +} diff --git a/src/Web/wwwroot/images/brand_dark.png b/src/Web/wwwroot/images/brand_dark.png deleted file mode 100644 index 44a65364f..000000000 Binary files a/src/Web/wwwroot/images/brand_dark.png and /dev/null differ diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index c649ab5ee..99e70ed48 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -1,14 +1,19 @@  - - netcoreapp1.1 + + netcoreapp2.0 + + 2.0.0 + + + - + diff --git a/tests/IntegrationTests/Infrastructure/File/LocalFileImageServiceGetImageBytesById.cs b/tests/IntegrationTests/Infrastructure/File/LocalFileImageServiceGetImageBytesById.cs deleted file mode 100644 index 77bc8eeda..000000000 --- a/tests/IntegrationTests/Infrastructure/File/LocalFileImageServiceGetImageBytesById.cs +++ /dev/null @@ -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 _mockEnvironment = new Mock(); - 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(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); - } -} -} diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj index eb5ec1258..f83abe608 100644 --- a/tests/IntegrationTests/IntegrationTests.csproj +++ b/tests/IntegrationTests/IntegrationTests.csproj @@ -1,14 +1,19 @@  - - netcoreapp1.1 + + netcoreapp2.0 + + 2.0.0 + + + - + diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index 92fe0172f..74503a3be 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -1,15 +1,18 @@  - - netcoreapp1.1 + + netcoreapp2.0 + + 2.0.0 + + - - + - +