From bff333eca3d97351ff9b2eb7d34774ee6a877bdd Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 8 Jul 2024 21:00:30 +0300 Subject: [PATCH 01/39] Add project Entities and Models --- Topers.Core/Models/Address.cs | 47 +++++++++++++++++ Topers.Core/Models/Customer.cs | 35 +++++++++++++ Topers.Core/Models/Good.cs | 49 +++++++++++++++++ Topers.Core/Models/GoodScope.cs | 35 +++++++++++++ Topers.Core/Models/Order.cs | 35 +++++++++++++ Topers.Core/Models/OrderDetails.cs | 41 +++++++++++++++ Topers.Core/Topers.Core.csproj | 4 ++ .../Entities/AddressEntity.cs | 47 +++++++++++++++++ .../Entities/CategoryEntity.cs | 27 ++++++++++ .../Entities/CustomerEntity.cs | 37 +++++++++++++ .../Entities/GoodEntity.cs | 52 +++++++++++++++++++ .../Entities/GoodScopeEntity.cs | 32 ++++++++++++ .../Entities/OrderDetailsEntity.cs | 42 +++++++++++++++ .../Entities/OrderEntity.cs | 37 +++++++++++++ .../Topers.DataAccess.Postgres.csproj | 4 ++ 15 files changed, 524 insertions(+) create mode 100644 Topers.Core/Models/Address.cs create mode 100644 Topers.Core/Models/Customer.cs create mode 100644 Topers.Core/Models/Good.cs create mode 100644 Topers.Core/Models/GoodScope.cs create mode 100644 Topers.Core/Models/Order.cs create mode 100644 Topers.Core/Models/OrderDetails.cs create mode 100644 Topers.DataAccess.Postgres/Entities/AddressEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/CategoryEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/CustomerEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/GoodEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/OrderEntity.cs diff --git a/Topers.Core/Models/Address.cs b/Topers.Core/Models/Address.cs new file mode 100644 index 0000000..83ddc70 --- /dev/null +++ b/Topers.Core/Models/Address.cs @@ -0,0 +1,47 @@ +namespace Topers.Core.Models; + +/// +/// Represents a customer address. +/// +public class Address +{ + public Address(Guid id, string street, string city, string state, string postalCode, string country) + { + Id = id; + Street = street; + City = city; + State = state; + PostalCode = postalCode; + Country = country; + } + + /// + /// Gets or sets an address identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets a customer street. + /// + public string Street { get; } = String.Empty; + + /// + /// Gets or sets a customer city. + /// + public string City { get; } = String.Empty; + + /// + /// Gets or sets a customer state. + /// + public string State { get; } = String.Empty; + + /// + /// Gets or sets a customer postal code. + /// + public string PostalCode { get; } = String.Empty; + + /// + /// Gets or sets a customer coutry. + /// + public string Country { get; } = String.Empty; +} \ No newline at end of file diff --git a/Topers.Core/Models/Customer.cs b/Topers.Core/Models/Customer.cs new file mode 100644 index 0000000..9ada7a0 --- /dev/null +++ b/Topers.Core/Models/Customer.cs @@ -0,0 +1,35 @@ +namespace Topers.Core.Models; + +/// +/// Represents a customer. +/// +public class Customer +{ + public Customer(Guid id, string name, string email, string phone) + { + Id = id; + Name = name; + Email = email; + Phone = phone; + } + + /// + /// Gets or sets a customer identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets a customer name. + /// + public string Name { get; } = string.Empty; + + /// + /// Gets or sets a customer identifier. + /// + public string Email { get; } = string.Empty; + + /// + /// Gets or sets a customer identifier. + /// + public string Phone { get; } = string.Empty; +} \ No newline at end of file diff --git a/Topers.Core/Models/Good.cs b/Topers.Core/Models/Good.cs new file mode 100644 index 0000000..f68cf8e --- /dev/null +++ b/Topers.Core/Models/Good.cs @@ -0,0 +1,49 @@ +namespace Topers.Core.Models; + +using Microsoft.AspNetCore.Http; + +/// +/// Represents a good. +/// +public class Good +{ + public Good(Guid id, Guid categoryId, string name, string description, string imageName, IFormFile image) + { + Id = id; + CategoryId = categoryId; + Name = name; + Description = description; + ImageName = imageName; + Image = image; + } + + /// + /// Gets or sets a good identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets a good category identifier. + /// + public Guid CategoryId { get; } + + /// + /// Gets or sets a good name. + /// + public string Name { get; } = string.Empty; + + /// + /// Gets or sets a good description. + /// + public string? Description { get; } = string.Empty; + + /// + /// Gets or sets a good image name file. + /// + public string? ImageName { get; } = string.Empty; + + /// + /// Gets or sets a good image. + /// + public IFormFile? Image { get; } +} \ No newline at end of file diff --git a/Topers.Core/Models/GoodScope.cs b/Topers.Core/Models/GoodScope.cs new file mode 100644 index 0000000..901840d --- /dev/null +++ b/Topers.Core/Models/GoodScope.cs @@ -0,0 +1,35 @@ +namespace Topers.Core.Models; + +/// +/// Represents a good scope entity. +/// +public class GoodScope +{ + public GoodScope(Guid id, Guid goodId, int litre, decimal price) + { + Id = id; + GoodId = goodId; + Litre = litre; + Price = price; + } + + /// + /// Gets or sets a good scope identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets a good identifier. + /// + public Guid GoodId { get; } + + /// + /// Gets or sets the volume in liters. + /// + public int Litre { get; } + + /// + /// Gets or sets the price. + /// + public decimal Price { get; } +} \ No newline at end of file diff --git a/Topers.Core/Models/Order.cs b/Topers.Core/Models/Order.cs new file mode 100644 index 0000000..6adf581 --- /dev/null +++ b/Topers.Core/Models/Order.cs @@ -0,0 +1,35 @@ +namespace Topers.Core.Models; + +/// +/// Represents an order. +/// +public class Order +{ + public Order(Guid id, DateTime date, Customer customer, decimal totalPrice) + { + Id = id; + Date = date; + Customer = customer; + TotalPrice = totalPrice; + } + + /// + /// Gets or sets an order identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets an order date. + /// + public DateTime Date { get; } + + /// + /// Gets or sets an order customer. + /// + public Customer Customer { get; } = null!; + + /// + /// Gets or sets an order total price. + /// + public decimal TotalPrice { get; } = 0; +} \ No newline at end of file diff --git a/Topers.Core/Models/OrderDetails.cs b/Topers.Core/Models/OrderDetails.cs new file mode 100644 index 0000000..501a970 --- /dev/null +++ b/Topers.Core/Models/OrderDetails.cs @@ -0,0 +1,41 @@ +namespace Topers.Core.Models; + +/// +/// Represents an order details. +/// +public class OrderDetails +{ + public OrderDetails(Guid id, Guid orderId, Guid goodId, int quantity, decimal price) + { + Id = id; + OrderId = orderId; + GoodId = goodId; + Quantity = quantity; + Price = price; + } + + /// + /// Gets or sets an order details identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets an order identifier. + /// + public Guid OrderId { get; } + + /// + /// Gets or sets a good identifier. + /// + public Guid GoodId { get; } + + /// + /// Gets or sets a good quantity. + /// + public int Quantity { get; } = 0; + + /// + /// Gets or sets a good price. + /// + public decimal Price { get; } = 0; +} \ No newline at end of file diff --git a/Topers.Core/Topers.Core.csproj b/Topers.Core/Topers.Core.csproj index bb23fb7..f726af0 100644 --- a/Topers.Core/Topers.Core.csproj +++ b/Topers.Core/Topers.Core.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/Topers.DataAccess.Postgres/Entities/AddressEntity.cs b/Topers.DataAccess.Postgres/Entities/AddressEntity.cs new file mode 100644 index 0000000..ab0d567 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/AddressEntity.cs @@ -0,0 +1,47 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a address entity. +/// +public class AddressEntity +{ + /// + /// Gets or sets a address identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a customer street. + /// + public string Street { get; set; } = string.Empty; + + /// + /// Gets or sets a customer city. + /// + public string City { get; set; } = string.Empty; + + /// + /// Gets or sets a customer state. + /// + public string State { get; set; } = string.Empty; + + /// + /// Gets or sets a customer postal code. + /// + public string PostalCode { get; set; } = string.Empty; + + /// + /// Gets or sets a customer country. + /// + public string Country { get; set; } = string.Empty; + + /// + /// Gets or sets a customer identifier. + /// + public Guid CustomerId { get; set; } + + /// + /// Gets or sets a customer. + /// + public CustomerEntity Customer { get; set; } = null!; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/CategoryEntity.cs b/Topers.DataAccess.Postgres/Entities/CategoryEntity.cs new file mode 100644 index 0000000..1373fb7 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/CategoryEntity.cs @@ -0,0 +1,27 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a category entity. +/// +public class CategoryEntity +{ + /// + /// Gets or sets a category identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a category name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets a category description. + /// + public string? Description { get; set; } = string.Empty; + + /// + /// Gets or sets a category goods. + /// + public ICollection Goods { get; set; } = []; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs new file mode 100644 index 0000000..c93e3e4 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs @@ -0,0 +1,37 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a customer entity. +/// +public class CustomerEntity +{ + /// + /// Gets or sets a customer identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a customer name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets a customer email. + /// + public string Email { get; set; } = string.Empty; + + /// + /// Gets or sets a customer phone. + /// + public string Phone { get; set; } = string.Empty; + + /// + /// Gets or sets a customer address identifier. + /// + public Guid AddressId { get; set; } + + /// + /// Gets or sets a customer address. + /// + public AddressEntity? Address { get; set; } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs new file mode 100644 index 0000000..2d7c051 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs @@ -0,0 +1,52 @@ +namespace Topers.DataAccess.Postgres.Entities; + +using Microsoft.AspNetCore.Http; + +/// +/// Represents a good entity. +/// +public class GoodEntity +{ + /// + /// Gets or sets a good identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a good name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets a good description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets a good image name file. + /// + public string? ImageName { get; set; } = string.Empty; + + /// + /// Gets or sets a good image. + /// + public IFormFile? Image { get; set; } + + /// + /// Gets or sets a good category identifier. + /// + public Guid CategoryId { get; set; } + + /// + /// Gets or sets a good category; + /// + public CategoryEntity Category { get; set; } = null!; + + /// + /// Gets or sets a good scopes collection. + /// + public ICollection Scopes { get; set; } = []; + + + // public ICollection<> +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs new file mode 100644 index 0000000..04155b6 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs @@ -0,0 +1,32 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a good scope entity. +/// +public class GoodScopeEntity +{ + /// + /// Gets or sets a good scope identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a good identifier. + /// + public Guid GoodId { get; set; } + + /// + /// Gets or sets a good. + /// + public GoodEntity Good { get; set; } = null!; + + /// + /// Gets or sets the volume in liters. + /// + public int Litre { get; set; } + + /// + /// Gets or sets the price. + /// + public decimal Price { get; set; } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs b/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs new file mode 100644 index 0000000..65a2723 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs @@ -0,0 +1,42 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents an order details. +/// +public class OrderDetailsEntity +{ + /// + /// Gets or sets an order details identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets an order identifier. + /// + public Guid OrderId { get; set; } + + /// + /// Gets or sets an order. + /// + public OrderEntity Order { get; set; } = null!; + + /// + /// Gets or sets a good identifier. + /// + public Guid GoodId { get; set; } + + /// + /// Gets or sets a good. + /// + public GoodEntity Good { get; set; } = null!; + + /// + /// Gets or sets a good quantity. + /// + public int Quantity { get; set; } = 0; + + /// + /// Gets or sets a good price. + /// + public decimal Price { get; set; } = 0; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/OrderEntity.cs b/Topers.DataAccess.Postgres/Entities/OrderEntity.cs new file mode 100644 index 0000000..bd7f428 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/OrderEntity.cs @@ -0,0 +1,37 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents an order. +/// +public class OrderEntity +{ + /// + /// Gets or sets an order identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets an order date. + /// + public DateTime Date { get; set; } + + /// + /// Gets or sets an order customer identifier. + /// + public Guid CustomerId { get; set; } + + /// + /// Gets or sets an order customer. + /// + public CustomerEntity Customer { get; set; } = null!; + + /// + /// Gets or sets an order total price. + /// + public decimal TotalPrice { get; set; } = 0; + + /// + /// Gets or sets an order details. + /// + public ICollection? OrderDetails { get; set; } = []; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj index bb23fb7..5ead741 100644 --- a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj +++ b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj @@ -6,4 +6,8 @@ enable + + + + From 74d6cb02ee2f471a874cdafea33b65c126825539 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 8 Jul 2024 22:10:02 +0300 Subject: [PATCH 02/39] Implement entities configurations and TopersDbContext --- Topers.Api/Program.cs | 8 + Topers.Api/Topers.Api.csproj | 10 + Topers.Api/appsettings.json | 3 + .../Configurations/AddressConfiguration.cs | 17 + .../Configurations/CategoryConfiguration.cs | 17 + .../Configurations/CustomerConfiguration.cs | 21 ++ .../Configurations/GoodConfiguration.cs | 25 ++ .../Configurations/GoodScopeConfiguration.cs | 17 + .../Configurations/OrderConfiguration.cs | 21 ++ .../OrderDetailsConfiguration.cs | 21 ++ .../Entities/CustomerEntity.cs | 5 + .../Entities/GoodEntity.cs | 8 +- .../20240708190433_Initial.Designer.cs | 299 ++++++++++++++++++ .../Migrations/20240708190433_Initial.cs | 210 ++++++++++++ .../TopersDbContextModelSnapshot.cs | 296 +++++++++++++++++ .../Topers.DataAccess.Postgres.csproj | 32 +- Topers.DataAccess.Postgres/TopersDbContext.cs | 29 ++ 17 files changed, 1026 insertions(+), 13 deletions(-) create mode 100644 Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/CategoryConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/GoodScopeConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/OrderConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs create mode 100644 Topers.DataAccess.Postgres/TopersDbContext.cs diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 42af6c7..11203c2 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -1,7 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Topers.DataAccess.Postgres; + var builder = WebApplication.CreateBuilder(args); { builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + + builder.Services.AddDbContext(options => + { + options.UseNpgsql(builder.Configuration.GetConnectionString("TopersDbContext")); + }); }; var app = builder.Build(); diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index fc545ba..080ad05 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -6,9 +6,19 @@ enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/Topers.Api/appsettings.json b/Topers.Api/appsettings.json index 4d56694..c89face 100644 --- a/Topers.Api/appsettings.json +++ b/Topers.Api/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, + "ConnectionStrings": { + "TopersDbContext": "Host=localhost; Database=topers; Username=postgres; Password=root" + }, "AllowedHosts": "*" } diff --git a/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs new file mode 100644 index 0000000..465b896 --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class AddressConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(a => a.Id); + builder + .HasOne(a => a.Customer) + .WithOne(c => c.Address) + .HasForeignKey(a => a.CustomerId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/CategoryConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/CategoryConfiguration.cs new file mode 100644 index 0000000..12996ea --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/CategoryConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class CategoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(c => c.Id); + builder + .HasMany(c => c.Goods) + .WithOne(g => g.Category) + .HasForeignKey(g => g.CategoryId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs new file mode 100644 index 0000000..a4c15f9 --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class CustomerConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(c => c.Id); + builder + .HasMany(c => c.Orders) + .WithOne(o => o.Customer) + .HasForeignKey(o => o.CustomerId); + builder + .HasOne(c => c.Address) + .WithOne(a => a.Customer) + .HasForeignKey(c => c.AddressId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs new file mode 100644 index 0000000..3e23d8b --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class GoodConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(g => g.Id); + builder + .HasOne(g => g.Category) + .WithMany(c => c.Goods) + .HasForeignKey(g => g.CategoryId); + builder + .HasMany(g => g.Scopes) + .WithOne(s => s.Good) + .HasForeignKey(s => s.GoodId); + builder + .HasMany(g => g.OrderDetails) + .WithOne(d => d.Good) + .HasForeignKey(d => d.GoodId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/GoodScopeConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/GoodScopeConfiguration.cs new file mode 100644 index 0000000..7d81359 --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/GoodScopeConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class GoodScopeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(s => s.Id); + builder + .HasOne(s => s.Good) + .WithMany(g => g.Scopes) + .HasForeignKey(s => s.GoodId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/OrderConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/OrderConfiguration.cs new file mode 100644 index 0000000..45d158c --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/OrderConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(o => o.Id); + builder + .HasOne(o => o.Customer) + .WithMany(c => c.Orders) + .HasForeignKey(o => o.CustomerId); + builder + .HasMany(o => o.OrderDetails) + .WithOne(d => d.Order) + .HasForeignKey(d => d.OrderId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs new file mode 100644 index 0000000..86f3e07 --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class OrderDetailsConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(d => d.Id); + builder + .HasOne(d => d.Order) + .WithMany(o => o.OrderDetails) + .HasForeignKey(d => d.OrderId); + builder + .HasOne(d => d.Good) + .WithMany(g => g.OrderDetails) + .HasForeignKey(d => d.GoodId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs index c93e3e4..c15734a 100644 --- a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs @@ -34,4 +34,9 @@ public class CustomerEntity /// Gets or sets a customer address. /// public AddressEntity? Address { get; set; } + + /// + /// Gets or sets a customer orders. + /// + public ICollection Orders { get; set; } = []; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs index 2d7c051..842d073 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs @@ -1,5 +1,6 @@ namespace Topers.DataAccess.Postgres.Entities; +using System.ComponentModel.DataAnnotations.Schema; using Microsoft.AspNetCore.Http; /// @@ -27,6 +28,7 @@ public class GoodEntity /// public string? ImageName { get; set; } = string.Empty; + [NotMapped] /// /// Gets or sets a good image. /// @@ -47,6 +49,8 @@ public class GoodEntity /// public ICollection Scopes { get; set; } = []; - - // public ICollection<> + /// + /// Gets or sets an order details about good. + /// + public ICollection? OrderDetails { get; set; } = []; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.Designer.cs new file mode 100644 index 0000000..2b43f57 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.Designer.cs @@ -0,0 +1,299 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240708190433_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AddressId") + .IsUnique(); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.AddressEntity", "Address") + .WithOne("Customer") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.CustomerEntity", "AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Navigation("Customer") + .IsRequired(); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("OrderDetails"); + + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.cs b/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.cs new file mode 100644 index 0000000..f1c1b42 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240708190433_Initial.cs @@ -0,0 +1,210 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Addresses", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Street = table.Column(type: "text", nullable: false), + City = table.Column(type: "text", nullable: false), + State = table.Column(type: "text", nullable: false), + PostalCode = table.Column(type: "text", nullable: false), + Country = table.Column(type: "text", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Addresses", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Phone = table.Column(type: "text", nullable: false), + AddressId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + table.ForeignKey( + name: "FK_Customers_Addresses_AddressId", + column: x => x.AddressId, + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Goods", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + ImageName = table.Column(type: "text", nullable: true), + CategoryId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Goods", x => x.Id); + table.ForeignKey( + name: "FK_Goods_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Date = table.Column(type: "timestamp with time zone", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + TotalPrice = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "GoodScopes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GoodId = table.Column(type: "uuid", nullable: false), + Litre = table.Column(type: "integer", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GoodScopes", x => x.Id); + table.ForeignKey( + name: "FK_GoodScopes_Goods_GoodId", + column: x => x.GoodId, + principalTable: "Goods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderDetails", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrderId = table.Column(type: "uuid", nullable: false), + GoodId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderDetails", x => x.Id); + table.ForeignKey( + name: "FK_OrderDetails_Goods_GoodId", + column: x => x.GoodId, + principalTable: "Goods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderDetails_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Customers_AddressId", + table: "Customers", + column: "AddressId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Goods_CategoryId", + table: "Goods", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_GoodScopes_GoodId", + table: "GoodScopes", + column: "GoodId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderDetails_GoodId", + table: "OrderDetails", + column: "GoodId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderDetails_OrderId", + table: "OrderDetails", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GoodScopes"); + + migrationBuilder.DropTable( + name: "OrderDetails"); + + migrationBuilder.DropTable( + name: "Goods"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "Categories"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Addresses"); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs new file mode 100644 index 0000000..56962d4 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -0,0 +1,296 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + partial class TopersDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddressId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AddressId") + .IsUnique(); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.AddressEntity", "Address") + .WithOne("Customer") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.CustomerEntity", "AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Address"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Navigation("Customer") + .IsRequired(); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("OrderDetails"); + + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj index 5ead741..0f599d0 100644 --- a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj +++ b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj @@ -1,13 +1,23 @@ - - - - net8.0 - enable - enable - - + + + + net8.0 + enable + enable + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/Topers.DataAccess.Postgres/TopersDbContext.cs b/Topers.DataAccess.Postgres/TopersDbContext.cs new file mode 100644 index 0000000..33f716f --- /dev/null +++ b/Topers.DataAccess.Postgres/TopersDbContext.cs @@ -0,0 +1,29 @@ +namespace Topers.DataAccess.Postgres; + +using Microsoft.EntityFrameworkCore; +using Topers.DataAccess.Postgres.Configurations; +using Topers.DataAccess.Postgres.Entities; + +public class TopersDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Categories { get; set; } + public DbSet Goods { get; set; } + public DbSet GoodScopes { get; set; } + public DbSet Addresses { get; set; } + public DbSet Customers { get; set; } + public DbSet Orders { get; set; } + public DbSet OrderDetails { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new CategoryConfiguration()); + modelBuilder.ApplyConfiguration(new GoodConfiguration()); + modelBuilder.ApplyConfiguration(new GoodScopeConfiguration()); + modelBuilder.ApplyConfiguration(new AddressConfiguration()); + modelBuilder.ApplyConfiguration(new CustomerConfiguration()); + modelBuilder.ApplyConfiguration(new OrderConfiguration()); + modelBuilder.ApplyConfiguration(new OrderDetailsConfiguration()); + + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file From 99eb9b0f31dcfef70b111d7c0e16d2851849a1fd Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Tue, 9 Jul 2024 10:05:37 +0300 Subject: [PATCH 03/39] Implement repository pattern & AutoMapper --- Topers.Api/Mapping/MappingProfile.cs | 16 ++++ Topers.Api/Program.cs | 9 ++ Topers.Api/Topers.Api.csproj | 2 + .../Abstractions/IAddressesRepository.cs | 8 ++ .../Abstractions/ICategoriesRespository.cs | 13 +++ .../Abstractions/ICustomersRepository.cs | 10 ++ Topers.Core/Abstractions/IGoodsRepository.cs | 13 +++ Topers.Core/Models/Category.cs | 29 ++++++ .../Repositories/AddressesRepository.cs | 34 +++++++ .../Repositories/CategoriesRepository.cs | 87 +++++++++++++++++ .../Repositories/CustomersRepository.cs | 58 +++++++++++ .../Repositories/GoodsRepository.cs | 96 +++++++++++++++++++ .../Topers.DataAccess.Postgres.csproj | 5 + 13 files changed, 380 insertions(+) create mode 100644 Topers.Api/Mapping/MappingProfile.cs create mode 100644 Topers.Core/Abstractions/IAddressesRepository.cs create mode 100644 Topers.Core/Abstractions/ICategoriesRespository.cs create mode 100644 Topers.Core/Abstractions/ICustomersRepository.cs create mode 100644 Topers.Core/Abstractions/IGoodsRepository.cs create mode 100644 Topers.Core/Models/Category.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs new file mode 100644 index 0000000..77b5362 --- /dev/null +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.Api.Mapping; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 11203c2..a2a693c 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -1,5 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; using Topers.DataAccess.Postgres; +using Topers.DataAccess.Postgres.Repositories; var builder = WebApplication.CreateBuilder(args); { @@ -10,6 +12,13 @@ { options.UseNpgsql(builder.Configuration.GetConnectionString("TopersDbContext")); }); + + builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); }; var app = builder.Build(); diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index 080ad05..98d6a05 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -8,9 +8,11 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Topers.Core/Abstractions/IAddressesRepository.cs b/Topers.Core/Abstractions/IAddressesRepository.cs new file mode 100644 index 0000000..780f953 --- /dev/null +++ b/Topers.Core/Abstractions/IAddressesRepository.cs @@ -0,0 +1,8 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface IAddressesRepository +{ + Task CreateAsync(Address address); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICategoriesRespository.cs b/Topers.Core/Abstractions/ICategoriesRespository.cs new file mode 100644 index 0000000..f0d6d3e --- /dev/null +++ b/Topers.Core/Abstractions/ICategoriesRespository.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface ICategoriesRepository +{ + Task CreateAsync(Category category); + Task> GetAllAsync(); + Task GetByIdAsync(Guid categoryId); + Task> GetGoodsByIdAsync(Guid categoryId); + Task UpdateAsync(Category category); + Task DeleteAsync(Guid categoryId); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICustomersRepository.cs b/Topers.Core/Abstractions/ICustomersRepository.cs new file mode 100644 index 0000000..45b82dd --- /dev/null +++ b/Topers.Core/Abstractions/ICustomersRepository.cs @@ -0,0 +1,10 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface ICustomersRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(Guid customerId); + Task CreateAsync(Customer customer); +} \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsRepository.cs b/Topers.Core/Abstractions/IGoodsRepository.cs new file mode 100644 index 0000000..23c8d96 --- /dev/null +++ b/Topers.Core/Abstractions/IGoodsRepository.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface IGoodsRepository +{ + Task CreateAsync(Good good); + Task> GetAllAsync(); + Task GetByIdAsync(Guid goodId); + Task> GetByFilterAsync(string title); + Task UpdateAsync(Good good); + Task DeleteAsync(Guid goodId); +}; \ No newline at end of file diff --git a/Topers.Core/Models/Category.cs b/Topers.Core/Models/Category.cs new file mode 100644 index 0000000..c5f35a6 --- /dev/null +++ b/Topers.Core/Models/Category.cs @@ -0,0 +1,29 @@ +namespace Topers.Core.Models; + +/// +/// Represents a category. +/// +public class Category +{ + public Category(Guid id, string name, string description) + { + Id = id; + Name = name; + Description = description; + } + + /// + /// Gets or sets a category identifier. + /// + public Guid Id { get; } + + /// + /// Gets or sets a category name. + /// + public string Name { get; } = string.Empty; + + /// + /// Gets or sets a category description. + /// + public string? Description { get; } = string.Empty; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs new file mode 100644 index 0000000..6e0ea68 --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs @@ -0,0 +1,34 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class AddressesRepository : IAddressesRepository +{ + private readonly TopersDbContext _context; + + public AddressesRepository(TopersDbContext context) + { + _context = context; + } + + public async Task CreateAsync(Address address) + { + var addressEntity = new AddressEntity + { + Id = Guid.NewGuid(), + Street = address.Street, + City = address.City, + State = address.State, + PostalCode = address.PostalCode, + Country = address.Country + }; + + await _context.Addresses.AddAsync(addressEntity); + await _context.SaveChangesAsync(); + + return addressEntity.Id; + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs b/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs new file mode 100644 index 0000000..5ab67ad --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs @@ -0,0 +1,87 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class CategoriesRepository : ICategoriesRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public CategoriesRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task CreateAsync(Category category) + { + var categoryEntity = new CategoryEntity + { + Id = Guid.NewGuid(), + Name = category.Name, + Description = category.Description + }; + + await _context.Categories.AddAsync(categoryEntity); + await _context.SaveChangesAsync(); + + return categoryEntity.Id; + } + + public async Task DeleteAsync(Guid categoryId) + { + await _context.Categories + .Where(c => c.Id == categoryId) + .ExecuteDeleteAsync(); + + return categoryId; + } + + public async Task> GetAllAsync() + { + var categoryEntities = await _context.Categories + .AsNoTracking() + .ToListAsync(); + + var categoryEntitiesDto = _mapper.Map>(categoryEntities); + + return categoryEntitiesDto; + } + + public async Task GetByIdAsync(Guid categoryId) + { + var categoryEntity = await _context.Categories + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == categoryId); + + var categoryEntityDto = _mapper.Map(categoryEntity); + + return categoryEntityDto; + } + + public async Task> GetGoodsByIdAsync(Guid categoryId) + { + var goodEntities = await _context.Goods + .Where(g => g.CategoryId == categoryId) + .ToListAsync(); + + var goodEntitiesDto = _mapper.Map>(goodEntities); + + return goodEntitiesDto; + } + + public async Task UpdateAsync(Category category) + { + await _context.Categories + .Where(c => c.Id == category.Id) + .ExecuteUpdateAsync(cUpdate => cUpdate + .SetProperty(c => c.Name, category.Name) + .SetProperty(c => c.Description, category.Description)); + + return category.Id; + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs b/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs new file mode 100644 index 0000000..2502010 --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs @@ -0,0 +1,58 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class CustomersRepository : ICustomersRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public CustomersRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task CreateAsync(Customer customer) + { + var customerEntity = new CustomerEntity + { + Id = Guid.NewGuid(), + Name = customer.Name, + Email = customer.Email, + Phone = customer.Phone + }; + + await _context.Customers.AddAsync(customerEntity); + await _context.SaveChangesAsync(); + + return customerEntity.Id; + } + + public async Task> GetAllAsync() + { + var customerEntities = await _context.Customers + .Include(c => c.Address) + .AsNoTracking() + .ToListAsync(); + + var customerEntitiesDto = _mapper.Map>(customerEntities); + + return customerEntitiesDto; + } + + public async Task GetByIdAsync(Guid customerId) + { + var customerEntity = await _context.Customers + .Include(c => c.Address) + .FirstOrDefaultAsync(c => c.Id == customerId); + + var customerEntityDto = _mapper.Map(customerEntity); + + return customerEntityDto; + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs new file mode 100644 index 0000000..d79601b --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -0,0 +1,96 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class GoodsRepository : IGoodsRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public GoodsRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task CreateAsync(Good good) + { + var goodEntity = new GoodEntity + { + Id = Guid.NewGuid(), + CategoryId = good.CategoryId, + Name = good.Name, + Description = good.Description, + ImageName = good.ImageName, + Image = good.Image + }; + + await _context.Goods.AddAsync(goodEntity); + await _context.SaveChangesAsync(); + + return goodEntity.Id; + } + + public async Task DeleteAsync(Guid goodId) + { + await _context.Goods + .Where(g => g.Id == goodId) + .ExecuteDeleteAsync(); + + return goodId; + } + + public async Task> GetAllAsync() + { + var goodEntities = await _context.Goods + .AsNoTracking() + .ToListAsync(); + + var goodEntitiesDto = _mapper.Map>(goodEntities); + + return goodEntitiesDto; + } + + public async Task> GetByFilterAsync(string title) + { + var query = _context.Goods.Include(g => g.Scopes).AsNoTracking(); + + if(!string.IsNullOrEmpty(title)) + { + query = query.Where(g => g.Name.Contains(title)); + } + + var goodEntitiesDto = _mapper.Map>(await query.ToListAsync()); + + return goodEntitiesDto; + } + + public async Task GetByIdAsync(Guid goodId) + { + var goodEntity = await _context.Goods + .AsNoTracking() + .FirstOrDefaultAsync(g => g.Id == goodId); + + var goodEntityDto = _mapper.Map(goodEntity); + + return goodEntityDto; + } + + public async Task UpdateAsync(Good good) + { + await _context.Goods + .Where(g => g.Id == good.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(g => g.Name, good.Name) + .SetProperty(g => g.CategoryId, good.CategoryId) + .SetProperty(g => g.Description, good.Description) + .SetProperty(g => g.ImageName, good.ImageName) + .SetProperty(g => g.Image, good.Image)); + + return good.Id; + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj index 0f599d0..df4c70e 100644 --- a/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj +++ b/Topers.DataAccess.Postgres/Topers.DataAccess.Postgres.csproj @@ -7,6 +7,11 @@ + + + + + From 81e7b00c6fcaef8aa033b5efd2c9af7bcebf893e Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Tue, 9 Jul 2024 11:12:36 +0300 Subject: [PATCH 04/39] Implement Dtos. Add Services --- Topers.Api/Program.cs | 6 +++ Topers.Api/Topers.Api.csproj | 1 + Topers.Core/Abstractions/IAddressesService.cs | 9 ++++ .../Abstractions/ICategoriesService.cs | 14 ++++++ Topers.Core/Abstractions/ICustomersService.cs | 11 +++++ Topers.Core/Abstractions/IGoodsService.cs | 13 +++++ Topers.Core/Dtos/AddressDto.cs | 19 ++++++++ Topers.Core/Dtos/CategoryDto.cs | 12 +++++ Topers.Core/Dtos/CustomerDto.cs | 15 ++++++ Topers.Core/Dtos/GoodDto.cs | 19 ++++++++ Topers.Core/Dtos/GoodScopeDto.cs | 14 ++++++ Topers.Core/Dtos/OrderDetailsDto.cs | 16 +++++++ Topers.Core/Dtos/OrderDto.cs | 14 ++++++ Topers.Core/Models/Address.cs | 8 +++- .../Services/AddressesService.cs | 25 ++++++++++ .../Services/CategoriesService.cs | 48 +++++++++++++++++++ .../Services/CustomersService.cs | 33 +++++++++++++ .../Services/GoodsService.cs | 43 +++++++++++++++++ .../Topers.Infrastructure.csproj | 7 +++ 19 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 Topers.Core/Abstractions/IAddressesService.cs create mode 100644 Topers.Core/Abstractions/ICategoriesService.cs create mode 100644 Topers.Core/Abstractions/ICustomersService.cs create mode 100644 Topers.Core/Abstractions/IGoodsService.cs create mode 100644 Topers.Core/Dtos/AddressDto.cs create mode 100644 Topers.Core/Dtos/CategoryDto.cs create mode 100644 Topers.Core/Dtos/CustomerDto.cs create mode 100644 Topers.Core/Dtos/GoodDto.cs create mode 100644 Topers.Core/Dtos/GoodScopeDto.cs create mode 100644 Topers.Core/Dtos/OrderDetailsDto.cs create mode 100644 Topers.Core/Dtos/OrderDto.cs create mode 100644 Topers.Infrastructure/Services/AddressesService.cs create mode 100644 Topers.Infrastructure/Services/CategoriesService.cs create mode 100644 Topers.Infrastructure/Services/CustomersService.cs create mode 100644 Topers.Infrastructure/Services/GoodsService.cs diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index a2a693c..4f9dbfa 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -2,6 +2,7 @@ using Topers.Core.Abstractions; using Topers.DataAccess.Postgres; using Topers.DataAccess.Postgres.Repositories; +using Topers.Infrastructure.Services; var builder = WebApplication.CreateBuilder(args); { @@ -19,6 +20,11 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); }; var app = builder.Build(); diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index 98d6a05..a7df09d 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -9,6 +9,7 @@ + diff --git a/Topers.Core/Abstractions/IAddressesService.cs b/Topers.Core/Abstractions/IAddressesService.cs new file mode 100644 index 0000000..bfa939a --- /dev/null +++ b/Topers.Core/Abstractions/IAddressesService.cs @@ -0,0 +1,9 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface IAddressesService +{ + Task AddAddressToCustomerAsync(Address address); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICategoriesService.cs b/Topers.Core/Abstractions/ICategoriesService.cs new file mode 100644 index 0000000..ad69521 --- /dev/null +++ b/Topers.Core/Abstractions/ICategoriesService.cs @@ -0,0 +1,14 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface ICategoriesService +{ + Task CreateCategoryAsync(Category category); + Task> GetAllCategoriesAsync(); + Task GetCategoryByIdAsync(Guid categoryId); + Task> GetGoodsByCategoryIdAsync(Guid categoryId); + Task UpdateCategoryAsync(Category category); + Task DeleteCategoryAsync(Guid categoryId); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICustomersService.cs b/Topers.Core/Abstractions/ICustomersService.cs new file mode 100644 index 0000000..46c1aa1 --- /dev/null +++ b/Topers.Core/Abstractions/ICustomersService.cs @@ -0,0 +1,11 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface ICustomersService +{ + Task> GetAllCustomersAsync(); + Task GetCustomerByIdAsync(Guid customerId); + Task CreateCustomerAsync(Customer customer); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsService.cs b/Topers.Core/Abstractions/IGoodsService.cs new file mode 100644 index 0000000..44a131d --- /dev/null +++ b/Topers.Core/Abstractions/IGoodsService.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface IGoodsService +{ + Task CreateGoodAsync(Good good); + Task> GetAllGoodsAsync(); + Task GetGoodByIdAsync(Guid goodId); + Task UpdateGoodAsync(Good good); + Task DeleteGoodAsync(Guid goodId); +}; \ No newline at end of file diff --git a/Topers.Core/Dtos/AddressDto.cs b/Topers.Core/Dtos/AddressDto.cs new file mode 100644 index 0000000..8257942 --- /dev/null +++ b/Topers.Core/Dtos/AddressDto.cs @@ -0,0 +1,19 @@ +namespace Topers.Core.Dtos; + +public record AddressResponseDto( + Guid Id, + Guid CustomerId, + string Street = "", + string City = "", + string State = "", + string PostalCode = "", + string Country = "" +); + +public record AddressRequestDto( + string Street = "", + string City = "", + string State = "", + string PostalCode = "", + string Country = "" +); \ No newline at end of file diff --git a/Topers.Core/Dtos/CategoryDto.cs b/Topers.Core/Dtos/CategoryDto.cs new file mode 100644 index 0000000..880df5e --- /dev/null +++ b/Topers.Core/Dtos/CategoryDto.cs @@ -0,0 +1,12 @@ +namespace Topers.Core.Dtos; + +public record CategoryResponseDto( + Guid Id, + string Name = "", + string Description = "" +); + +public record CategoryRequestDto( + string Name = "", + string Description = "" +); \ No newline at end of file diff --git a/Topers.Core/Dtos/CustomerDto.cs b/Topers.Core/Dtos/CustomerDto.cs new file mode 100644 index 0000000..7848773 --- /dev/null +++ b/Topers.Core/Dtos/CustomerDto.cs @@ -0,0 +1,15 @@ +namespace Topers.Core.Dtos; + +public record CustomerResponseDto( + Guid Id, + string Name = "", + string Email = "", + string Phone = "", + AddressResponseDto? Address = null +); + +public record CustomerRequestDto( + string Name = "", + string Email = "", + string Phone = "" +); \ No newline at end of file diff --git a/Topers.Core/Dtos/GoodDto.cs b/Topers.Core/Dtos/GoodDto.cs new file mode 100644 index 0000000..d3f133f --- /dev/null +++ b/Topers.Core/Dtos/GoodDto.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Http; + +namespace Topers.Core.Dtos; + +public record GoodResponseDto( + Guid Id, + string Name = "", + string Description = "", + string ImageName = "", + IFormFile? Image = null +); + +public record GoodRequestDto( + Guid CategoryId, + string Name = "", + string Description = "", + string ImageName = "", + IFormFile? Image = null +); \ No newline at end of file diff --git a/Topers.Core/Dtos/GoodScopeDto.cs b/Topers.Core/Dtos/GoodScopeDto.cs new file mode 100644 index 0000000..29750dd --- /dev/null +++ b/Topers.Core/Dtos/GoodScopeDto.cs @@ -0,0 +1,14 @@ +namespace Topers.Core.Dtos; + +public record GoodScopeResponseDto( + Guid Id, + Guid GoodId, + int Litre, + decimal Price +); + +public record GoodScopeRequestDto( + Guid GoodId, + int Litre, + decimal Price +); \ No newline at end of file diff --git a/Topers.Core/Dtos/OrderDetailsDto.cs b/Topers.Core/Dtos/OrderDetailsDto.cs new file mode 100644 index 0000000..9281441 --- /dev/null +++ b/Topers.Core/Dtos/OrderDetailsDto.cs @@ -0,0 +1,16 @@ +namespace Topers.Core.Dtos; + +public record OrderDetailsResponseDto( + Guid Id, + Guid OrderId, + Guid GoodId, + int Quantity, + decimal Price +); + +public record OrderDetailsRequestDto( + Guid OrderId, + Guid GoodId, + int Quantity, + decimal Price +); \ No newline at end of file diff --git a/Topers.Core/Dtos/OrderDto.cs b/Topers.Core/Dtos/OrderDto.cs new file mode 100644 index 0000000..02e2c07 --- /dev/null +++ b/Topers.Core/Dtos/OrderDto.cs @@ -0,0 +1,14 @@ +namespace Topers.Core.Dtos; + +public record OrderResponseDto( + Guid Id, + DateTime Date, + Guid CustomerId, + decimal TotalPrice +); + +public record OrderRequestDto( + DateTime Date, + Guid CustomerId, + decimal TotalPrice +); \ No newline at end of file diff --git a/Topers.Core/Models/Address.cs b/Topers.Core/Models/Address.cs index 83ddc70..b6e0e90 100644 --- a/Topers.Core/Models/Address.cs +++ b/Topers.Core/Models/Address.cs @@ -5,9 +5,10 @@ namespace Topers.Core.Models; /// public class Address { - public Address(Guid id, string street, string city, string state, string postalCode, string country) + public Address(Guid id, Guid customerId, string street, string city, string state, string postalCode, string country) { Id = id; + CustomerId = customerId; Street = street; City = city; State = state; @@ -20,6 +21,11 @@ public Address(Guid id, string street, string city, string state, string postalC /// public Guid Id { get; } + /// + /// Gets or sets a customer identifier. + /// + public Guid CustomerId { get; } + /// /// Gets or sets a customer street. /// diff --git a/Topers.Infrastructure/Services/AddressesService.cs b/Topers.Infrastructure/Services/AddressesService.cs new file mode 100644 index 0000000..cc03ab2 --- /dev/null +++ b/Topers.Infrastructure/Services/AddressesService.cs @@ -0,0 +1,25 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class AddressesService : IAddressesService +{ + private readonly IAddressesRepository _addressesRepository; + private readonly IMapper _mapper; + + public AddressesService(IAddressesRepository addressesRepository, IMapper mapper) + { + _addressesRepository = addressesRepository; + _mapper = mapper; + } + + public async Task AddAddressToCustomerAsync(Address address) + { + var addressEntity = await _addressesRepository.CreateAsync(address); + + return _mapper.Map(addressEntity); + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/CategoriesService.cs b/Topers.Infrastructure/Services/CategoriesService.cs new file mode 100644 index 0000000..b5c9a90 --- /dev/null +++ b/Topers.Infrastructure/Services/CategoriesService.cs @@ -0,0 +1,48 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class CategoriesService : ICategoriesService +{ + private readonly ICategoriesRepository _categoriesRepository; + private readonly IMapper _mapper; + + public CategoriesService(ICategoriesRepository categoriesRepository, IMapper mapper) + { + _categoriesRepository = categoriesRepository; + _mapper = mapper; + } + + public async Task CreateCategoryAsync(Category category) + { + return await _categoriesRepository.CreateAsync(category); + } + + public async Task DeleteCategoryAsync(Guid categoryId) + { + return await _categoriesRepository.DeleteAsync(categoryId); + } + + public async Task> GetAllCategoriesAsync() + { + return _mapper.Map>(await _categoriesRepository.GetAllAsync()); + } + + public async Task GetCategoryByIdAsync(Guid categoryId) + { + return _mapper.Map(await _categoriesRepository.GetByIdAsync(categoryId)); + } + + public async Task> GetGoodsByCategoryIdAsync(Guid categoryId) + { + return _mapper.Map>(await _categoriesRepository.GetGoodsByIdAsync(categoryId)); + } + + public async Task UpdateCategoryAsync(Category category) + { + return await _categoriesRepository.UpdateAsync(category); + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/CustomersService.cs b/Topers.Infrastructure/Services/CustomersService.cs new file mode 100644 index 0000000..e727961 --- /dev/null +++ b/Topers.Infrastructure/Services/CustomersService.cs @@ -0,0 +1,33 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class CustomersService : ICustomersService +{ + private readonly ICustomersRepository _customersRepository; + private readonly IMapper _mapper; + + public CustomersService(ICustomersRepository customersRepository, IMapper mapper) + { + _customersRepository = customersRepository; + _mapper = mapper; + } + + public async Task CreateCustomerAsync(Customer customer) + { + return await _customersRepository.CreateAsync(customer); + } + + public async Task> GetAllCustomersAsync() + { + return _mapper.Map>(await _customersRepository.GetAllAsync()); + } + + public async Task GetCustomerByIdAsync(Guid customerId) + { + return _mapper.Map(await _customersRepository.GetByIdAsync(customerId)); + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/GoodsService.cs b/Topers.Infrastructure/Services/GoodsService.cs new file mode 100644 index 0000000..4c6cf22 --- /dev/null +++ b/Topers.Infrastructure/Services/GoodsService.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class GoodsService : IGoodsService +{ + private readonly IGoodsRepository _goodsRepository; + private readonly IMapper _mapper; + + public GoodsService(IGoodsRepository goodsRepository, IMapper mapper) + { + _goodsRepository = goodsRepository; + _mapper = mapper; + } + + public async Task CreateGoodAsync(Good good) + { + return await _goodsRepository.CreateAsync(good); + } + + public async Task DeleteGoodAsync(Guid goodId) + { + return await _goodsRepository.DeleteAsync(goodId); + } + + public async Task> GetAllGoodsAsync() + { + return _mapper.Map>(await _goodsRepository.GetAllAsync()); + } + + public async Task GetGoodByIdAsync(Guid goodId) + { + return _mapper.Map(await _goodsRepository.GetByIdAsync(goodId)); + } + + public async Task UpdateGoodAsync(Good good) + { + return await _goodsRepository.UpdateAsync(good); + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Topers.Infrastructure.csproj b/Topers.Infrastructure/Topers.Infrastructure.csproj index bb23fb7..037b891 100644 --- a/Topers.Infrastructure/Topers.Infrastructure.csproj +++ b/Topers.Infrastructure/Topers.Infrastructure.csproj @@ -6,4 +6,11 @@ enable + + + + + + + From ac15805883ca762ed4fbc09aec88583a0d4fdd78 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Tue, 9 Jul 2024 16:14:26 +0300 Subject: [PATCH 05/39] Implement Controllers --- Topers.Api/Controllers/AddressesController.cs | 34 ++++++ .../Controllers/CategoriesController.cs | 105 ++++++++++++++++++ Topers.Api/Controllers/CustomersController.cs | 61 ++++++++++ Topers.Api/Controllers/GoodsController.cs | 91 +++++++++++++++ Topers.Api/Program.cs | 2 + Topers.Api/Topers.Api.csproj | 1 + Topers.Core/Dtos/AddressDto.cs | 1 + Topers.Core/Models/Good.cs | 2 +- 8 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 Topers.Api/Controllers/AddressesController.cs create mode 100644 Topers.Api/Controllers/CategoriesController.cs create mode 100644 Topers.Api/Controllers/CustomersController.cs create mode 100644 Topers.Api/Controllers/GoodsController.cs diff --git a/Topers.Api/Controllers/AddressesController.cs b/Topers.Api/Controllers/AddressesController.cs new file mode 100644 index 0000000..1fd3982 --- /dev/null +++ b/Topers.Api/Controllers/AddressesController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Api.Controllers; + +[ApiController] +[Route("api/addresses")] +public class AddressesController(IAddressesService addressesService) : ControllerBase +{ + private readonly IAddressesService _addressService = addressesService; + + [HttpPost("{customerId:guid}")] + [SwaggerResponse(200, Description = "Returns the new address data of the customer.", Type = typeof(AddressResponseDto))] + public async Task> AddAddressToCustomer([FromRoute] Guid customerId, [FromBody] AddressRequestDto address) + { + var newAddress = new Address + ( + Guid.Empty, + customerId, + address.Street, + address.City, + address.State, + address.PostalCode, + address.Country + ); + + var addressEntity = await _addressService.AddAddressToCustomerAsync(newAddress); + + return Ok(addressEntity); + } +} \ No newline at end of file diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs new file mode 100644 index 0000000..88fdc4a --- /dev/null +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -0,0 +1,105 @@ +namespace Topers.Api.Controllers; + +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +[ApiController] +[Route("api/categories")] +public class CategoriesController : ControllerBase +{ + private readonly ICategoriesService _categoryService; + + public CategoriesController(ICategoriesService categoryService) + { + _categoryService = categoryService; + } + + [HttpGet] + [SwaggerResponse(200, Description = "Returns a category list.", Type = typeof(IEnumerable))] + [SwaggerResponse(400)] + public async Task>> GetCategories() + { + var categories = await _categoryService.GetAllCategoriesAsync(); + + if (categories == null) + { + return BadRequest(); + } + + return Ok(categories); + } + + [HttpGet("{categoryId:guid}")] + [SwaggerResponse(200, Description = "Returns a category.", Type = typeof(CategoryResponseDto))] + [SwaggerResponse(400)] + public async Task> GetCategory([FromRoute] Guid categoryId) + { + var category = await _categoryService.GetCategoryByIdAsync(categoryId); + + if (category == null) + { + return BadRequest(); + } + + return Ok(category); + } + + [HttpGet("{categoryId:guid}/goods")] + [SwaggerResponse(200, Description = "Returns a category goods.", Type = typeof(IEnumerable))] + [SwaggerResponse(400)] + public async Task>> GetCategoryGoods([FromRoute] Guid categoryId) + { + var goods = await _categoryService.GetGoodsByCategoryIdAsync(categoryId); + + if (goods == null) + { + return BadRequest(); + } + + return Ok(goods); + } + + [HttpPost("create")] + [SwaggerResponse(200, Description = "Create a new category.")] + public async Task> CreateCategory([FromBody] CategoryRequestDto category) + { + var newCategory = new Category + ( + Guid.Empty, + category.Name, + category.Description + ); + + await _categoryService.CreateCategoryAsync(newCategory); + + return Ok(newCategory); + } + + [HttpPut("{categoryId:guid}")] + [SwaggerResponse(200, Description = "Update an existing category.")] + public async Task> UpdateCategory([FromRoute] Guid categoryId, [FromBody] CategoryRequestDto category) + { + var existCategory = new Category + ( + categoryId, + category.Name, + category.Description + ); + + var updatedCategory = await _categoryService.UpdateCategoryAsync(existCategory); + + return Ok(updatedCategory); + } + + [HttpDelete("{categoryId:guid}")] + [SwaggerResponse(200, Description = "Delete category.")] + public async Task> DeleteCategory([FromRoute] Guid categoryId) + { + await _categoryService.DeleteCategoryAsync(categoryId); + + return Ok(categoryId); + } +} \ No newline at end of file diff --git a/Topers.Api/Controllers/CustomersController.cs b/Topers.Api/Controllers/CustomersController.cs new file mode 100644 index 0000000..f5580e5 --- /dev/null +++ b/Topers.Api/Controllers/CustomersController.cs @@ -0,0 +1,61 @@ +namespace Topers.Api.Contollers; + +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +[ApiController] +[Route("api/customers")] +public class CustomersController(ICustomersService customerService) : ControllerBase +{ + private readonly ICustomersService _customerService = customerService; + + [HttpGet] + [SwaggerResponse(200, Description = "Returns a customer list.", Type = typeof(IEnumerable))] + [SwaggerResponse(400)] + public async Task>> GetCustomers() + { + var customers = await _customerService.GetAllCustomersAsync(); + + if (customers == null) + { + return BadRequest(); + } + + return Ok(customers); + } + + [HttpGet("{customerId:guid}")] + [SwaggerResponse(200, Description = "Returns a customer.", Type = typeof(CustomerResponseDto))] + [SwaggerResponse(400)] + public async Task> GetCustomerById([FromRoute] Guid customerId) + { + var customer = await _customerService.GetCustomerByIdAsync(customerId); + + if (customer == null) + { + return BadRequest(); + } + + return Ok(customer); + } + + [HttpPost("create")] + [SwaggerResponse(200, Description = "Create a new customer.")] + public async Task> CreateCustomer([FromBody] CustomerRequestDto customer) + { + var newCustomer = new Customer + ( + Guid.Empty, + customer.Name, + customer.Email, + customer.Phone + ); + + await _customerService.CreateCustomerAsync(newCustomer); + + return Ok(newCustomer.Id); + } +} diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs new file mode 100644 index 0000000..eccb022 --- /dev/null +++ b/Topers.Api/Controllers/GoodsController.cs @@ -0,0 +1,91 @@ +namespace Topers.Api.Contollers; + +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +[ApiController] +[Route("api/goods")] +public class GoodsController(IGoodsService goodService) : ControllerBase +{ + private readonly IGoodsService _goodService = goodService; + + [HttpGet] + [SwaggerResponse(200, Description = "Returns a good list.", Type = typeof(IEnumerable))] + [SwaggerResponse(400)] + public async Task>> GetGoods() + { + var goods = await _goodService.GetAllGoodsAsync(); + + if (goods == null) + { + return BadRequest(); + } + + return Ok(goods); + } + + [HttpGet("{goodId:guid}")] + [SwaggerResponse(200, Description = "Returns a good.", Type = typeof(GoodResponseDto))] + [SwaggerResponse(400)] + public async Task> GetGood([FromRoute] Guid goodId) + { + var good = await _goodService.GetGoodByIdAsync(goodId); + + if (good == null) + { + return BadRequest(); + } + + return Ok(good); + } + + [HttpPost("create")] + [SwaggerResponse(200, Description = "Create a new good.")] + public async Task> CreateGood([FromBody] GoodRequestDto good) + { + var newGood = new Good + ( + Guid.Empty, + good.CategoryId, + good.Name, + good.Description, + good.ImageName, + good.Image + ); + + await _goodService.CreateGoodAsync(newGood); + + return Ok(newGood); + } + + [HttpPut("{goodId:guid}")] + [SwaggerResponse(200, Description = "Update an existing good.")] + public async Task> UpdateGood([FromRoute] Guid goodId, [FromBody] GoodRequestDto good) + { + var existGood = new Good + ( + goodId, + good.CategoryId, + good.Name, + good.Description, + good.ImageName, + good.Image + ); + + var updatedGood = await _goodService.UpdateGoodAsync(existGood); + + return Ok(updatedGood); + } + + [HttpDelete("{goodId:guid}")] + [SwaggerResponse(200, Description = "Delete good.")] + public async Task> DeleteGood([FromRoute] Guid goodId) + { + await _goodService.DeleteGoodAsync(goodId); + + return Ok(goodId); + } +} \ No newline at end of file diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 4f9dbfa..412bc24 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -8,6 +8,7 @@ { builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + builder.Services.AddControllers(); builder.Services.AddDbContext(options => { @@ -41,5 +42,6 @@ } app.UseHttpsRedirection(); + app.MapControllers(); app.Run(); } \ No newline at end of file diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index a7df09d..d156156 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -22,6 +22,7 @@ + diff --git a/Topers.Core/Dtos/AddressDto.cs b/Topers.Core/Dtos/AddressDto.cs index 8257942..17728a7 100644 --- a/Topers.Core/Dtos/AddressDto.cs +++ b/Topers.Core/Dtos/AddressDto.cs @@ -11,6 +11,7 @@ public record AddressResponseDto( ); public record AddressRequestDto( + Guid CustomerId, string Street = "", string City = "", string State = "", diff --git a/Topers.Core/Models/Good.cs b/Topers.Core/Models/Good.cs index f68cf8e..95f9025 100644 --- a/Topers.Core/Models/Good.cs +++ b/Topers.Core/Models/Good.cs @@ -7,7 +7,7 @@ namespace Topers.Core.Models; /// public class Good { - public Good(Guid id, Guid categoryId, string name, string description, string imageName, IFormFile image) + public Good(Guid id, Guid categoryId, string name, string description, string imageName, IFormFile? image) { Id = id; CategoryId = categoryId; From e13888db440e5eead272ca262f36ac812ffceef6 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Tue, 9 Jul 2024 21:38:17 +0300 Subject: [PATCH 06/39] Fix mapper bugs and entities implementations --- .../Controllers/CategoriesController.cs | 4 +- Topers.Api/Controllers/CustomersController.cs | 5 +- Topers.Api/Mapping/MappingProfile.cs | 27 ++ .../Abstractions/ICategoriesService.cs | 2 +- Topers.Core/Abstractions/ICustomersService.cs | 2 +- Topers.Core/Dtos/AddressDto.cs | 1 - Topers.Core/Dtos/CategoryDto.cs | 2 +- Topers.Core/Models/Customer.cs | 8 +- .../Configurations/AddressConfiguration.cs | 1 + .../Configurations/CustomerConfiguration.cs | 4 - .../Entities/CustomerEntity.cs | 5 - ...41734_UpdateCustomersRefFields.Designer.cs | 292 ++++++++++++++++++ ...20240709141734_UpdateCustomersRefFields.cs | 74 +++++ .../TopersDbContextModelSnapshot.cs | 27 +- .../Repositories/AddressesRepository.cs | 1 + .../Services/AddressesService.cs | 20 +- .../Services/CategoriesService.cs | 13 +- .../Services/CustomersService.cs | 14 +- 18 files changed, 457 insertions(+), 45 deletions(-) create mode 100644 Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.cs diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs index 88fdc4a..298c774 100644 --- a/Topers.Api/Controllers/CategoriesController.cs +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -73,9 +73,9 @@ public async Task> CreateCategory([FromBody] C category.Description ); - await _categoryService.CreateCategoryAsync(newCategory); + var newCategoryEntity = await _categoryService.CreateCategoryAsync(newCategory); - return Ok(newCategory); + return Ok(newCategoryEntity); } [HttpPut("{categoryId:guid}")] diff --git a/Topers.Api/Controllers/CustomersController.cs b/Topers.Api/Controllers/CustomersController.cs index f5580e5..842b58e 100644 --- a/Topers.Api/Controllers/CustomersController.cs +++ b/Topers.Api/Controllers/CustomersController.cs @@ -49,13 +49,14 @@ public async Task> CreateCustomer([FromBody] CustomerRequestD var newCustomer = new Customer ( Guid.Empty, + null, customer.Name, customer.Email, customer.Phone ); - await _customerService.CreateCustomerAsync(newCustomer); + var newCustomerEntity = await _customerService.CreateCustomerAsync(newCustomer); - return Ok(newCustomer.Id); + return Ok(newCustomerEntity); } } diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index 77b5362..e6c6bfe 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Topers.Core.Dtos; using Topers.Core.Models; using Topers.DataAccess.Postgres.Entities; @@ -8,9 +9,35 @@ public class MappingProfile : Profile { public MappingProfile() { + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address != null ? new AddressResponseDto ( + src.Address.Id, + src.Id, + src.Address.Street, + src.Address.City, + src.Address.State, + src.Address.PostalCode, + src.Address.Country + ) : null)); + + CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); + + CreateMap() + .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address)); + + CreateMap() + .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address)); + + CreateMap(); } } \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICategoriesService.cs b/Topers.Core/Abstractions/ICategoriesService.cs index ad69521..247de09 100644 --- a/Topers.Core/Abstractions/ICategoriesService.cs +++ b/Topers.Core/Abstractions/ICategoriesService.cs @@ -5,7 +5,7 @@ namespace Topers.Core.Abstractions; public interface ICategoriesService { - Task CreateCategoryAsync(Category category); + Task CreateCategoryAsync(Category category); Task> GetAllCategoriesAsync(); Task GetCategoryByIdAsync(Guid categoryId); Task> GetGoodsByCategoryIdAsync(Guid categoryId); diff --git a/Topers.Core/Abstractions/ICustomersService.cs b/Topers.Core/Abstractions/ICustomersService.cs index 46c1aa1..dd8fb69 100644 --- a/Topers.Core/Abstractions/ICustomersService.cs +++ b/Topers.Core/Abstractions/ICustomersService.cs @@ -7,5 +7,5 @@ public interface ICustomersService { Task> GetAllCustomersAsync(); Task GetCustomerByIdAsync(Guid customerId); - Task CreateCustomerAsync(Customer customer); + Task CreateCustomerAsync(Customer customer); }; \ No newline at end of file diff --git a/Topers.Core/Dtos/AddressDto.cs b/Topers.Core/Dtos/AddressDto.cs index 17728a7..8257942 100644 --- a/Topers.Core/Dtos/AddressDto.cs +++ b/Topers.Core/Dtos/AddressDto.cs @@ -11,7 +11,6 @@ public record AddressResponseDto( ); public record AddressRequestDto( - Guid CustomerId, string Street = "", string City = "", string State = "", diff --git a/Topers.Core/Dtos/CategoryDto.cs b/Topers.Core/Dtos/CategoryDto.cs index 880df5e..ba62754 100644 --- a/Topers.Core/Dtos/CategoryDto.cs +++ b/Topers.Core/Dtos/CategoryDto.cs @@ -3,7 +3,7 @@ namespace Topers.Core.Dtos; public record CategoryResponseDto( Guid Id, string Name = "", - string Description = "" + string? Description = "" ); public record CategoryRequestDto( diff --git a/Topers.Core/Models/Customer.cs b/Topers.Core/Models/Customer.cs index 9ada7a0..456cc41 100644 --- a/Topers.Core/Models/Customer.cs +++ b/Topers.Core/Models/Customer.cs @@ -5,9 +5,10 @@ namespace Topers.Core.Models; /// public class Customer { - public Customer(Guid id, string name, string email, string phone) + public Customer(Guid id, Address? address, string name, string email, string phone) { Id = id; + Address = address; Name = name; Email = email; Phone = phone; @@ -18,6 +19,11 @@ public Customer(Guid id, string name, string email, string phone) /// public Guid Id { get; } + /// + /// Gets or sets a customer address. + /// + public Address? Address { get; } + /// /// Gets or sets a customer name. /// diff --git a/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs index 465b896..3279960 100644 --- a/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs +++ b/Topers.DataAccess.Postgres/Configurations/AddressConfiguration.cs @@ -13,5 +13,6 @@ public void Configure(EntityTypeBuilder builder) .HasOne(a => a.Customer) .WithOne(c => c.Address) .HasForeignKey(a => a.CustomerId); + builder.HasIndex(a => a.CustomerId).IsUnique(true); } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs index a4c15f9..84f959d 100644 --- a/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs +++ b/Topers.DataAccess.Postgres/Configurations/CustomerConfiguration.cs @@ -13,9 +13,5 @@ public void Configure(EntityTypeBuilder builder) .HasMany(c => c.Orders) .WithOne(o => o.Customer) .HasForeignKey(o => o.CustomerId); - builder - .HasOne(c => c.Address) - .WithOne(a => a.Customer) - .HasForeignKey(c => c.AddressId); } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs index c15734a..401a749 100644 --- a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs @@ -25,11 +25,6 @@ public class CustomerEntity /// public string Phone { get; set; } = string.Empty; - /// - /// Gets or sets a customer address identifier. - /// - public Guid AddressId { get; set; } - /// /// Gets or sets a customer address. /// diff --git a/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.Designer.cs new file mode 100644 index 0000000..641aa5b --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.Designer.cs @@ -0,0 +1,292 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240709141734_UpdateCustomersRefFields")] + partial class UpdateCustomersRefFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Address"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("OrderDetails"); + + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.cs b/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.cs new file mode 100644 index 0000000..7e3427e --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240709141734_UpdateCustomersRefFields.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class UpdateCustomersRefFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Customers_Addresses_AddressId", + table: "Customers"); + + migrationBuilder.DropIndex( + name: "IX_Customers_AddressId", + table: "Customers"); + + migrationBuilder.DropColumn( + name: "AddressId", + table: "Customers"); + + migrationBuilder.CreateIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses", + column: "CustomerId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Addresses_Customers_CustomerId", + table: "Addresses", + column: "CustomerId", + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Addresses_Customers_CustomerId", + table: "Addresses"); + + migrationBuilder.DropIndex( + name: "IX_Addresses_CustomerId", + table: "Addresses"); + + migrationBuilder.AddColumn( + name: "AddressId", + table: "Customers", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateIndex( + name: "IX_Customers_AddressId", + table: "Customers", + column: "AddressId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Customers_Addresses_AddressId", + table: "Customers", + column: "AddressId", + principalTable: "Addresses", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs index 56962d4..60f9aa3 100644 --- a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -53,6 +53,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CustomerId") + .IsUnique(); + b.ToTable("Addresses"); }); @@ -80,9 +83,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AddressId") - .HasColumnType("uuid"); - b.Property("Email") .IsRequired() .HasColumnType("text"); @@ -97,9 +97,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("AddressId") - .IsUnique(); - b.ToTable("Customers"); }); @@ -200,15 +197,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Orders"); }); - modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => { - b.HasOne("Topers.DataAccess.Postgres.Entities.AddressEntity", "Address") - .WithOne("Customer") - .HasForeignKey("Topers.DataAccess.Postgres.Entities.CustomerEntity", "AddressId") + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Address"); + b.Navigation("Customer"); }); modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => @@ -263,12 +260,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Customer"); }); - modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => - { - b.Navigation("Customer") - .IsRequired(); - }); - modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => { b.Navigation("Goods"); @@ -276,6 +267,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => { + b.Navigation("Address"); + b.Navigation("Orders"); }); diff --git a/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs index 6e0ea68..7eb2d3b 100644 --- a/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs @@ -19,6 +19,7 @@ public async Task CreateAsync(Address address) var addressEntity = new AddressEntity { Id = Guid.NewGuid(), + CustomerId = address.CustomerId, Street = address.Street, City = address.City, State = address.State, diff --git a/Topers.Infrastructure/Services/AddressesService.cs b/Topers.Infrastructure/Services/AddressesService.cs index cc03ab2..e650fd3 100644 --- a/Topers.Infrastructure/Services/AddressesService.cs +++ b/Topers.Infrastructure/Services/AddressesService.cs @@ -1,4 +1,3 @@ -using AutoMapper; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; @@ -8,18 +7,27 @@ namespace Topers.Infrastructure.Services; public class AddressesService : IAddressesService { private readonly IAddressesRepository _addressesRepository; - private readonly IMapper _mapper; - public AddressesService(IAddressesRepository addressesRepository, IMapper mapper) + public AddressesService(IAddressesRepository addressesRepository) { _addressesRepository = addressesRepository; - _mapper = mapper; } public async Task AddAddressToCustomerAsync(Address address) { - var addressEntity = await _addressesRepository.CreateAsync(address); + var addressEntityIdentifier = await _addressesRepository.CreateAsync(address); - return _mapper.Map(addressEntity); + var newAddressEntity = new AddressResponseDto + ( + addressEntityIdentifier, + address.CustomerId, + address.Street, + address.City, + address.State, + address.PostalCode, + address.Country + ); + + return newAddressEntity; } } \ No newline at end of file diff --git a/Topers.Infrastructure/Services/CategoriesService.cs b/Topers.Infrastructure/Services/CategoriesService.cs index b5c9a90..83c2432 100644 --- a/Topers.Infrastructure/Services/CategoriesService.cs +++ b/Topers.Infrastructure/Services/CategoriesService.cs @@ -16,9 +16,18 @@ public CategoriesService(ICategoriesRepository categoriesRepository, IMapper map _mapper = mapper; } - public async Task CreateCategoryAsync(Category category) + public async Task CreateCategoryAsync(Category category) { - return await _categoriesRepository.CreateAsync(category); + var newCategoryIdentifier = await _categoriesRepository.CreateAsync(category); + + var newCategory = new CategoryResponseDto + ( + newCategoryIdentifier, + category.Name, + category.Description + ); + + return newCategory; } public async Task DeleteCategoryAsync(Guid categoryId) diff --git a/Topers.Infrastructure/Services/CustomersService.cs b/Topers.Infrastructure/Services/CustomersService.cs index e727961..5502b33 100644 --- a/Topers.Infrastructure/Services/CustomersService.cs +++ b/Topers.Infrastructure/Services/CustomersService.cs @@ -16,9 +16,19 @@ public CustomersService(ICustomersRepository customersRepository, IMapper mapper _mapper = mapper; } - public async Task CreateCustomerAsync(Customer customer) + public async Task CreateCustomerAsync(Customer customer) { - return await _customersRepository.CreateAsync(customer); + var newCustomerIdentifier = await _customersRepository.CreateAsync(customer); + + var newCustomer = new CustomerResponseDto + ( + newCustomerIdentifier, + customer.Name, + customer.Email, + customer.Phone + ); + + return newCustomer; } public async Task> GetAllCustomersAsync() From 37c4ca035ba7a7bd04327f5c31e3ea2d029db419 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Wed, 10 Jul 2024 08:25:43 +0300 Subject: [PATCH 07/39] Implement Fluent validators --- Topers.Api/Topers.Api.csproj | 1 + Topers.Core/Topers.Core.csproj | 1 + Topers.Core/Validators/AddressDtoValidator.cs | 32 +++++++++++++++++++ .../Validators/CategoryDtoValidator.cs | 15 +++++++++ .../Validators/CustomerDtoValidator.cs | 23 +++++++++++++ Topers.Core/Validators/GoodDtoValidator.cs | 18 +++++++++++ .../Validators/GoodScopeDtoValidator.cs | 25 +++++++++++++++ .../Validators/OrderDetailsDtoValidator.cs | 25 +++++++++++++++ Topers.Core/Validators/OrderDtoValidator.cs | 20 ++++++++++++ 9 files changed, 160 insertions(+) create mode 100644 Topers.Core/Validators/AddressDtoValidator.cs create mode 100644 Topers.Core/Validators/CategoryDtoValidator.cs create mode 100644 Topers.Core/Validators/CustomerDtoValidator.cs create mode 100644 Topers.Core/Validators/GoodDtoValidator.cs create mode 100644 Topers.Core/Validators/GoodScopeDtoValidator.cs create mode 100644 Topers.Core/Validators/OrderDetailsDtoValidator.cs create mode 100644 Topers.Core/Validators/OrderDtoValidator.cs diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index d156156..5f6aff6 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -14,6 +14,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Topers.Core/Topers.Core.csproj b/Topers.Core/Topers.Core.csproj index f726af0..d7650b9 100644 --- a/Topers.Core/Topers.Core.csproj +++ b/Topers.Core/Topers.Core.csproj @@ -7,6 +7,7 @@ + diff --git a/Topers.Core/Validators/AddressDtoValidator.cs b/Topers.Core/Validators/AddressDtoValidator.cs new file mode 100644 index 0000000..97dc3eb --- /dev/null +++ b/Topers.Core/Validators/AddressDtoValidator.cs @@ -0,0 +1,32 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class AddressDtoValidator : AbstractValidator +{ + public AddressDtoValidator() + { + RuleFor(a => a.Street) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(80).WithMessage("{PropertyName} must not exceed 50 characters!"); + RuleFor(a => a.City) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(60).WithMessage("{PropertyName} must not exceed 50 characters!"); + RuleFor(a => a.State) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(60).WithMessage("{PropertyName} must not exceed 50 characters!"); + RuleFor(a => a.PostalCode) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MinimumLength(5).WithMessage("{PropertyName} must not be lower than 5 characters!") + .MaximumLength(5).WithMessage("{PropertyName} must not exceed 5 characters!"); + RuleFor(a => a.Country) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(60).WithMessage("{PropertyName} must not exceed 50 characters!"); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/CategoryDtoValidator.cs b/Topers.Core/Validators/CategoryDtoValidator.cs new file mode 100644 index 0000000..1033150 --- /dev/null +++ b/Topers.Core/Validators/CategoryDtoValidator.cs @@ -0,0 +1,15 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class CategoryDtoValidator : AbstractValidator +{ + public CategoryDtoValidator() + { + RuleFor(c => c.Name) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(60).WithMessage("{PropertyName} must not exceed 60 characters!"); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/CustomerDtoValidator.cs b/Topers.Core/Validators/CustomerDtoValidator.cs new file mode 100644 index 0000000..6ac0e52 --- /dev/null +++ b/Topers.Core/Validators/CustomerDtoValidator.cs @@ -0,0 +1,23 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class CustomerDtoValidator : AbstractValidator +{ + public CustomerDtoValidator() + { + RuleFor(c => c.Name) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(100).WithMessage("{PropertyName} must not exceed 60 characters!"); + RuleFor(c => c.Email) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .EmailAddress().WithMessage("A valid {PropertyName} is required!"); + RuleFor(c => c.Phone) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .Matches(@"^(((\+44\s?\d{4}|\(?0\d{4}\)?)\s?\d{3}\s?\d{3})|((\+44\s?\d{3}|\(?0\d{3}\)?)\s?\d{3}\s?\d{4})|((\+44\s?\d{2}|\(?0\d{2}\)?)\s?\d{4}\s?\d{4}))(\s?\#(\d{4}|\d{3}))?$").WithMessage("A valid {PropertyName} is required!"); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/GoodDtoValidator.cs b/Topers.Core/Validators/GoodDtoValidator.cs new file mode 100644 index 0000000..2c0fe7c --- /dev/null +++ b/Topers.Core/Validators/GoodDtoValidator.cs @@ -0,0 +1,18 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class GoodDtoValidator : AbstractValidator +{ + public GoodDtoValidator() + { + RuleFor(g => g.CategoryId) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(g => g.Name) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .MaximumLength(120).WithMessage("{PropertyName} must not exceed 120 characters!"); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/GoodScopeDtoValidator.cs b/Topers.Core/Validators/GoodScopeDtoValidator.cs new file mode 100644 index 0000000..82c237d --- /dev/null +++ b/Topers.Core/Validators/GoodScopeDtoValidator.cs @@ -0,0 +1,25 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class GoodScopeDtoValidator : AbstractValidator +{ + public GoodScopeDtoValidator() + { + RuleFor(g => g.GoodId) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(g => g.Litre) + .Must(IsLitreValid).WithMessage("Litre must be one of the following values: 1, 5, 10, 15, 25, 1000!"); + RuleFor(g => g.Price) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + } + + private bool IsLitreValid(int litre) + { + int[] validLitres = { 1, 5, 10, 15, 25, 1000 }; + return validLitres.Contains(litre); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/OrderDetailsDtoValidator.cs b/Topers.Core/Validators/OrderDetailsDtoValidator.cs new file mode 100644 index 0000000..3afe6cc --- /dev/null +++ b/Topers.Core/Validators/OrderDetailsDtoValidator.cs @@ -0,0 +1,25 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; +using System.Data; + +public class OrderDetailsDtoValidator : AbstractValidator +{ + public OrderDetailsDtoValidator() + { + RuleFor(d => d.OrderId) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(d => d.GoodId) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(d => d.Quantity) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull() + .GreaterThan(0).WithMessage("A product {PropertyName} must be greater than 0!"); + RuleFor(d => d.Price) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + } +} \ No newline at end of file diff --git a/Topers.Core/Validators/OrderDtoValidator.cs b/Topers.Core/Validators/OrderDtoValidator.cs new file mode 100644 index 0000000..0390660 --- /dev/null +++ b/Topers.Core/Validators/OrderDtoValidator.cs @@ -0,0 +1,20 @@ +namespace Topers.Core.Validators; + +using Topers.Core.Dtos; +using FluentValidation; + +public class OrderDtoValidator : AbstractValidator +{ + public OrderDtoValidator() + { + RuleFor(o => o.CustomerId) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(o => o.Date) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + RuleFor(o => o.TotalPrice) + .NotEmpty().WithMessage("{PropertyName} is required!") + .NotNull(); + } +} \ No newline at end of file From 3ef302d164446f9b58d8dc9039034a109da61928 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Wed, 10 Jul 2024 19:16:30 +0300 Subject: [PATCH 08/39] Add validation configurations to controllers --- Topers.Api/Controllers/AddressesController.cs | 10 ++++++++++ .../Controllers/CategoriesController.cs | 19 +++++++++++++++++++ Topers.Api/Controllers/CustomersController.cs | 10 ++++++++++ Topers.Api/Controllers/GoodsController.cs | 19 +++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/Topers.Api/Controllers/AddressesController.cs b/Topers.Api/Controllers/AddressesController.cs index 1fd3982..a2ee805 100644 --- a/Topers.Api/Controllers/AddressesController.cs +++ b/Topers.Api/Controllers/AddressesController.cs @@ -3,6 +3,7 @@ using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; +using Topers.Core.Validators; namespace Topers.Api.Controllers; @@ -16,6 +17,15 @@ public class AddressesController(IAddressesService addressesService) : Controlle [SwaggerResponse(200, Description = "Returns the new address data of the customer.", Type = typeof(AddressResponseDto))] public async Task> AddAddressToCustomer([FromRoute] Guid customerId, [FromBody] AddressRequestDto address) { + var newAddressValidator = new AddressDtoValidator(); + + var newAddressValidatorResult = newAddressValidator.Validate(address); + + if (!newAddressValidatorResult.IsValid) + { + return BadRequest(newAddressValidatorResult.Errors); + } + var newAddress = new Address ( Guid.Empty, diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs index 298c774..51f233f 100644 --- a/Topers.Api/Controllers/CategoriesController.cs +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -5,6 +5,7 @@ namespace Topers.Api.Controllers; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; +using Topers.Core.Validators; [ApiController] [Route("api/categories")] @@ -66,6 +67,15 @@ public async Task>> GetCategoryGoods([FromRou [SwaggerResponse(200, Description = "Create a new category.")] public async Task> CreateCategory([FromBody] CategoryRequestDto category) { + var categoryValidator = new CategoryDtoValidator(); + + var categoryValidatorResult = categoryValidator.Validate(category); + + if (!categoryValidatorResult.IsValid) + { + return BadRequest(categoryValidatorResult.Errors); + } + var newCategory = new Category ( Guid.Empty, @@ -82,6 +92,15 @@ public async Task> CreateCategory([FromBody] C [SwaggerResponse(200, Description = "Update an existing category.")] public async Task> UpdateCategory([FromRoute] Guid categoryId, [FromBody] CategoryRequestDto category) { + var categoryValidator = new CategoryDtoValidator(); + + var categoryValidatorResult = categoryValidator.Validate(category); + + if (!categoryValidatorResult.IsValid) + { + return BadRequest(categoryValidatorResult.Errors); + } + var existCategory = new Category ( categoryId, diff --git a/Topers.Api/Controllers/CustomersController.cs b/Topers.Api/Controllers/CustomersController.cs index 842b58e..013ca1a 100644 --- a/Topers.Api/Controllers/CustomersController.cs +++ b/Topers.Api/Controllers/CustomersController.cs @@ -5,6 +5,7 @@ namespace Topers.Api.Contollers; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; +using Topers.Core.Validators; [ApiController] [Route("api/customers")] @@ -46,6 +47,15 @@ public async Task> GetCustomerById([FromRoute] [SwaggerResponse(200, Description = "Create a new customer.")] public async Task> CreateCustomer([FromBody] CustomerRequestDto customer) { + var newCustomerValidator = new CustomerDtoValidator(); + + var newCustomerValidatorResult = newCustomerValidator.Validate(customer); + + if (!newCustomerValidatorResult.IsValid) + { + return BadRequest(newCustomerValidatorResult.Errors); + } + var newCustomer = new Customer ( Guid.Empty, diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index eccb022..9734f95 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -5,6 +5,7 @@ namespace Topers.Api.Contollers; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; +using Topers.Core.Validators; [ApiController] [Route("api/goods")] @@ -46,6 +47,15 @@ public async Task> GetGood([FromRoute] Guid goodId [SwaggerResponse(200, Description = "Create a new good.")] public async Task> CreateGood([FromBody] GoodRequestDto good) { + var newGoodValidator = new GoodDtoValidator(); + + var newGoodValidatorResult = newGoodValidator.Validate(good); + + if (!newGoodValidatorResult.IsValid) + { + return BadRequest(newGoodValidatorResult.Errors); + } + var newGood = new Good ( Guid.Empty, @@ -65,6 +75,15 @@ public async Task> CreateGood([FromBody] GoodReque [SwaggerResponse(200, Description = "Update an existing good.")] public async Task> UpdateGood([FromRoute] Guid goodId, [FromBody] GoodRequestDto good) { + var goodValidator = new GoodDtoValidator(); + + var goodValidatorResult = goodValidator.Validate(good); + + if (!goodValidatorResult.IsValid) + { + return BadRequest(goodValidatorResult.Errors); + } + var existGood = new Good ( goodId, From e6d619b766f1e9c96981626e7b4c4d355cd9c1a4 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Wed, 10 Jul 2024 20:03:18 +0300 Subject: [PATCH 09/39] Fix SwaggerResponse status-codes & result descriptions --- Topers.Api/Controllers/AddressesController.cs | 1 + Topers.Api/Controllers/CategoriesController.cs | 8 +++++--- Topers.Api/Controllers/CustomersController.cs | 5 +++-- Topers.Api/Controllers/GoodsController.cs | 6 ++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Topers.Api/Controllers/AddressesController.cs b/Topers.Api/Controllers/AddressesController.cs index a2ee805..db93d52 100644 --- a/Topers.Api/Controllers/AddressesController.cs +++ b/Topers.Api/Controllers/AddressesController.cs @@ -15,6 +15,7 @@ public class AddressesController(IAddressesService addressesService) : Controlle [HttpPost("{customerId:guid}")] [SwaggerResponse(200, Description = "Returns the new address data of the customer.", Type = typeof(AddressResponseDto))] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> AddAddressToCustomer([FromRoute] Guid customerId, [FromBody] AddressRequestDto address) { var newAddressValidator = new AddressDtoValidator(); diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs index 51f233f..bfb2f7e 100644 --- a/Topers.Api/Controllers/CategoriesController.cs +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -20,7 +20,7 @@ public CategoriesController(ICategoriesService categoryService) [HttpGet] [SwaggerResponse(200, Description = "Returns a category list.", Type = typeof(IEnumerable))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Categories not found.")] public async Task>> GetCategories() { var categories = await _categoryService.GetAllCategoriesAsync(); @@ -35,7 +35,7 @@ public async Task>> GetCategories() [HttpGet("{categoryId:guid}")] [SwaggerResponse(200, Description = "Returns a category.", Type = typeof(CategoryResponseDto))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Category not found.")] public async Task> GetCategory([FromRoute] Guid categoryId) { var category = await _categoryService.GetCategoryByIdAsync(categoryId); @@ -50,7 +50,7 @@ public async Task> GetCategory([FromRoute] Gui [HttpGet("{categoryId:guid}/goods")] [SwaggerResponse(200, Description = "Returns a category goods.", Type = typeof(IEnumerable))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Goods not found.")] public async Task>> GetCategoryGoods([FromRoute] Guid categoryId) { var goods = await _categoryService.GetGoodsByCategoryIdAsync(categoryId); @@ -65,6 +65,7 @@ public async Task>> GetCategoryGoods([FromRou [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new category.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> CreateCategory([FromBody] CategoryRequestDto category) { var categoryValidator = new CategoryDtoValidator(); @@ -90,6 +91,7 @@ public async Task> CreateCategory([FromBody] C [HttpPut("{categoryId:guid}")] [SwaggerResponse(200, Description = "Update an existing category.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> UpdateCategory([FromRoute] Guid categoryId, [FromBody] CategoryRequestDto category) { var categoryValidator = new CategoryDtoValidator(); diff --git a/Topers.Api/Controllers/CustomersController.cs b/Topers.Api/Controllers/CustomersController.cs index 013ca1a..3959c48 100644 --- a/Topers.Api/Controllers/CustomersController.cs +++ b/Topers.Api/Controllers/CustomersController.cs @@ -15,7 +15,7 @@ public class CustomersController(ICustomersService customerService) : Controller [HttpGet] [SwaggerResponse(200, Description = "Returns a customer list.", Type = typeof(IEnumerable))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Customers not found.")] public async Task>> GetCustomers() { var customers = await _customerService.GetAllCustomersAsync(); @@ -30,7 +30,7 @@ public async Task>> GetCustomers() [HttpGet("{customerId:guid}")] [SwaggerResponse(200, Description = "Returns a customer.", Type = typeof(CustomerResponseDto))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Customer not found.")] public async Task> GetCustomerById([FromRoute] Guid customerId) { var customer = await _customerService.GetCustomerByIdAsync(customerId); @@ -45,6 +45,7 @@ public async Task> GetCustomerById([FromRoute] [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new customer.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> CreateCustomer([FromBody] CustomerRequestDto customer) { var newCustomerValidator = new CustomerDtoValidator(); diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index 9734f95..574b14c 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -15,7 +15,7 @@ public class GoodsController(IGoodsService goodService) : ControllerBase [HttpGet] [SwaggerResponse(200, Description = "Returns a good list.", Type = typeof(IEnumerable))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Goods not found.")] public async Task>> GetGoods() { var goods = await _goodService.GetAllGoodsAsync(); @@ -30,7 +30,7 @@ public async Task>> GetGoods() [HttpGet("{goodId:guid}")] [SwaggerResponse(200, Description = "Returns a good.", Type = typeof(GoodResponseDto))] - [SwaggerResponse(400)] + [SwaggerResponse(400, Description = "Good not found.")] public async Task> GetGood([FromRoute] Guid goodId) { var good = await _goodService.GetGoodByIdAsync(goodId); @@ -45,6 +45,7 @@ public async Task> GetGood([FromRoute] Guid goodId [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new good.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> CreateGood([FromBody] GoodRequestDto good) { var newGoodValidator = new GoodDtoValidator(); @@ -73,6 +74,7 @@ public async Task> CreateGood([FromBody] GoodReque [HttpPut("{goodId:guid}")] [SwaggerResponse(200, Description = "Update an existing good.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> UpdateGood([FromRoute] Guid goodId, [FromBody] GoodRequestDto good) { var goodValidator = new GoodDtoValidator(); From 0484bc3092ee3619a670d527e4911b19fbb9d8b3 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Thu, 11 Jul 2024 11:57:17 +0300 Subject: [PATCH 10/39] Add User model. Set up an AuthController. --- Topers.Api/Controllers/AuthController.cs | 41 ++++++++++++++++++++++++ Topers.Api/Topers.Api.csproj | 1 + Topers.Core/Dtos/UserDto.cs | 6 ++++ Topers.Core/Models/User.cs | 19 +++++++++++ 4 files changed, 67 insertions(+) create mode 100644 Topers.Api/Controllers/AuthController.cs create mode 100644 Topers.Core/Dtos/UserDto.cs create mode 100644 Topers.Core/Models/User.cs diff --git a/Topers.Api/Controllers/AuthController.cs b/Topers.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..5b78949 --- /dev/null +++ b/Topers.Api/Controllers/AuthController.cs @@ -0,0 +1,41 @@ +using BCrypt.Net; +using Microsoft.AspNetCore.Mvc; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Api.Controllers; + +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + public static User _user = new User(); + + [HttpPost("register")] + public ActionResult Register([FromBody] UserDto request) + { + string passwordHash + = BCrypt.Net.BCrypt.HashPassword(request.Password); + + _user.Username = request.Username; + _user.PasswordHash = passwordHash; + + return Ok(_user); + } + + [HttpPost("login")] + public ActionResult Login([FromBody] UserDto request) + { + if (_user.Username != request.Username) + { + return BadRequest("User not found!"); + } + + if (!BCrypt.Net.BCrypt.Verify(request.Password, _user.PasswordHash)) + { + return BadRequest("Wrong Password!"); + } + + return Ok(_user); + } +} \ No newline at end of file diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index 5f6aff6..95069fc 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -14,6 +14,7 @@ + diff --git a/Topers.Core/Dtos/UserDto.cs b/Topers.Core/Dtos/UserDto.cs new file mode 100644 index 0000000..e2ecd52 --- /dev/null +++ b/Topers.Core/Dtos/UserDto.cs @@ -0,0 +1,6 @@ +namespace Topers.Core.Dtos; + +public record UserDto( + string Username = "", + string Password = "" +); \ No newline at end of file diff --git a/Topers.Core/Models/User.cs b/Topers.Core/Models/User.cs new file mode 100644 index 0000000..30ded8f --- /dev/null +++ b/Topers.Core/Models/User.cs @@ -0,0 +1,19 @@ +namespace Topers.Core.Models; + +/// +/// Represents a user. +/// +public class User +{ + public User() { } + + /// + /// Gets a username. + /// + public string Username { get; set; } = string.Empty; + + /// + /// Gets a hash password. + /// + public string PasswordHash { get; set; } = string.Empty; +} \ No newline at end of file From c803ba5e69ec79803018a3c51e6936b97e5de233 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Thu, 11 Jul 2024 22:51:10 +0300 Subject: [PATCH 11/39] Integrate JWT-token settings --- Topers.Api/Controllers/AccountController.cs | 29 ++ Topers.Api/Controllers/AuthController.cs | 41 --- .../Controllers/CategoriesController.cs | 4 + Topers.Api/Extensions/ApiExtensions.cs | 31 ++ Topers.Api/Mapping/MappingProfile.cs | 2 + Topers.Api/Program.cs | 14 + Topers.Api/appsettings.json | 6 +- Topers.Core/Abstractions/IJwtProvider.cs | 8 + Topers.Core/Abstractions/IPasswordHasher.cs | 7 + Topers.Core/Abstractions/IUsersRepository.cs | 9 + Topers.Core/Abstractions/IUsersService.cs | 7 + Topers.Core/Dtos/UserDto.cs | 14 +- Topers.Core/Models/User.cs | 33 +- .../Entities/UserEntity.cs | 27 ++ .../20240711155413_AddUserEntity.Designer.cs | 315 ++++++++++++++++++ .../20240711155413_AddUserEntity.cs | 36 ++ .../TopersDbContextModelSnapshot.cs | 23 ++ .../Repositories/UsersRepository.cs | 43 +++ Topers.DataAccess.Postgres/TopersDbContext.cs | 1 + Topers.Infrastructure/Features/JwtOptions.cs | 7 + Topers.Infrastructure/Features/JwtProvider.cs | 33 ++ .../Features/PasswordHasher.cs | 12 + .../Services/UsersService.cs | 47 +++ .../Topers.Infrastructure.csproj | 2 + 24 files changed, 701 insertions(+), 50 deletions(-) create mode 100644 Topers.Api/Controllers/AccountController.cs delete mode 100644 Topers.Api/Controllers/AuthController.cs create mode 100644 Topers.Api/Extensions/ApiExtensions.cs create mode 100644 Topers.Core/Abstractions/IJwtProvider.cs create mode 100644 Topers.Core/Abstractions/IPasswordHasher.cs create mode 100644 Topers.Core/Abstractions/IUsersRepository.cs create mode 100644 Topers.Core/Abstractions/IUsersService.cs create mode 100644 Topers.DataAccess.Postgres/Entities/UserEntity.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/UsersRepository.cs create mode 100644 Topers.Infrastructure/Features/JwtOptions.cs create mode 100644 Topers.Infrastructure/Features/JwtProvider.cs create mode 100644 Topers.Infrastructure/Features/PasswordHasher.cs create mode 100644 Topers.Infrastructure/Services/UsersService.cs diff --git a/Topers.Api/Controllers/AccountController.cs b/Topers.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..d717599 --- /dev/null +++ b/Topers.Api/Controllers/AccountController.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; + + +namespace Topers.Api.Controllers; + +[ApiController] +[Route("api/account")] +public class AccountController(IUsersService userService) : ControllerBase +{ + private readonly IUsersService _userService = userService; + + [HttpPost("register")] + public async Task Register([FromBody] RegisterUserRequestDto request) + { + await _userService.Register(request.Username, request.Email, request.Password); + + return Results.Ok(); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginUserRequestDto request) + { + var token = await _userService.Login(request.Username, request.Password); + + return Results.Ok(token); + } +} \ No newline at end of file diff --git a/Topers.Api/Controllers/AuthController.cs b/Topers.Api/Controllers/AuthController.cs deleted file mode 100644 index 5b78949..0000000 --- a/Topers.Api/Controllers/AuthController.cs +++ /dev/null @@ -1,41 +0,0 @@ -using BCrypt.Net; -using Microsoft.AspNetCore.Mvc; -using Topers.Core.Dtos; -using Topers.Core.Models; - -namespace Topers.Api.Controllers; - -[ApiController] -[Route("api/auth")] -public class AuthController : ControllerBase -{ - public static User _user = new User(); - - [HttpPost("register")] - public ActionResult Register([FromBody] UserDto request) - { - string passwordHash - = BCrypt.Net.BCrypt.HashPassword(request.Password); - - _user.Username = request.Username; - _user.PasswordHash = passwordHash; - - return Ok(_user); - } - - [HttpPost("login")] - public ActionResult Login([FromBody] UserDto request) - { - if (_user.Username != request.Username) - { - return BadRequest("User not found!"); - } - - if (!BCrypt.Net.BCrypt.Verify(request.Password, _user.PasswordHash)) - { - return BadRequest("Wrong Password!"); - } - - return Ok(_user); - } -} \ No newline at end of file diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs index bfb2f7e..4990567 100644 --- a/Topers.Api/Controllers/CategoriesController.cs +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -1,5 +1,7 @@ namespace Topers.Api.Controllers; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using Topers.Core.Abstractions; @@ -33,6 +35,7 @@ public async Task>> GetCategories() return Ok(categories); } + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [HttpGet("{categoryId:guid}")] [SwaggerResponse(200, Description = "Returns a category.", Type = typeof(CategoryResponseDto))] [SwaggerResponse(400, Description = "Category not found.")] @@ -48,6 +51,7 @@ public async Task> GetCategory([FromRoute] Gui return Ok(category); } + [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [HttpGet("{categoryId:guid}/goods")] [SwaggerResponse(200, Description = "Returns a category goods.", Type = typeof(IEnumerable))] [SwaggerResponse(400, Description = "Goods not found.")] diff --git a/Topers.Api/Extensions/ApiExtensions.cs b/Topers.Api/Extensions/ApiExtensions.cs new file mode 100644 index 0000000..c6b96c3 --- /dev/null +++ b/Topers.Api/Extensions/ApiExtensions.cs @@ -0,0 +1,31 @@ +namespace Topers.Api.Extensions; + +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Topers.Infrastructure.Features; + +public static class ApiExtensions +{ + public static void AddApiAuthentication( + this IServiceCollection services, + IOptions jwtOptions) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtOptions.Value.SecretKey)) + }; + }); + + services.AddAuthorization(); + } +} \ No newline at end of file diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index e6c6bfe..e33b4b8 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -31,6 +31,8 @@ public MappingProfile() CreateMap(); CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); CreateMap() .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address)); diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 412bc24..7bd6357 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -2,14 +2,21 @@ using Topers.Core.Abstractions; using Topers.DataAccess.Postgres; using Topers.DataAccess.Postgres.Repositories; +using Topers.Infrastructure.Features; using Topers.Infrastructure.Services; +using Topers.Api.Extensions; +using Microsoft.Extensions.Options; var builder = WebApplication.CreateBuilder(args); { + var jwtOptionsSection = builder.Configuration.GetSection(nameof(JwtOptions)); + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddControllers(); + builder.Services.Configure(builder.Configuration.GetSection(nameof(JwtOptions))); + builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("TopersDbContext")); @@ -21,11 +28,16 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + + builder.Services.AddScoped(); + builder.Services.AddScoped(); }; var app = builder.Build(); @@ -42,6 +54,8 @@ } app.UseHttpsRedirection(); + app.UseAuthentication(); + app.UseAuthorization(); app.MapControllers(); app.Run(); } \ No newline at end of file diff --git a/Topers.Api/appsettings.json b/Topers.Api/appsettings.json index c89face..1658c5c 100644 --- a/Topers.Api/appsettings.json +++ b/Topers.Api/appsettings.json @@ -8,5 +8,9 @@ "ConnectionStrings": { "TopersDbContext": "Host=localhost; Database=topers; Username=postgres; Password=root" }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "JwtOptions": { + "SecretKey": "secretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey", + "ExpiresHours": "12" + } } diff --git a/Topers.Core/Abstractions/IJwtProvider.cs b/Topers.Core/Abstractions/IJwtProvider.cs new file mode 100644 index 0000000..c9bee91 --- /dev/null +++ b/Topers.Core/Abstractions/IJwtProvider.cs @@ -0,0 +1,8 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface IJwtProvider +{ + string GenerateToken(User user); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IPasswordHasher.cs b/Topers.Core/Abstractions/IPasswordHasher.cs new file mode 100644 index 0000000..35f8b2e --- /dev/null +++ b/Topers.Core/Abstractions/IPasswordHasher.cs @@ -0,0 +1,7 @@ +namespace Topers.Core.Abstractions; + +public interface IPasswordHasher +{ + string Generate(string password); + bool Verify(string password, string hashedPassword); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IUsersRepository.cs b/Topers.Core/Abstractions/IUsersRepository.cs new file mode 100644 index 0000000..142b7f7 --- /dev/null +++ b/Topers.Core/Abstractions/IUsersRepository.cs @@ -0,0 +1,9 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface IUsersRepository +{ + Task Add(User user); + Task GetByName(string username); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IUsersService.cs b/Topers.Core/Abstractions/IUsersService.cs new file mode 100644 index 0000000..a0a4737 --- /dev/null +++ b/Topers.Core/Abstractions/IUsersService.cs @@ -0,0 +1,7 @@ +namespace Topers.Core.Abstractions; + +public interface IUsersService +{ + Task Register(string username, string email, string password); + Task Login(string username, string password); +}; \ No newline at end of file diff --git a/Topers.Core/Dtos/UserDto.cs b/Topers.Core/Dtos/UserDto.cs index e2ecd52..aca386c 100644 --- a/Topers.Core/Dtos/UserDto.cs +++ b/Topers.Core/Dtos/UserDto.cs @@ -1,6 +1,14 @@ +using System.ComponentModel.DataAnnotations; + namespace Topers.Core.Dtos; -public record UserDto( - string Username = "", - string Password = "" +public record LoginUserRequestDto( + [Required] string Username = "", + [Required] string Password = "" +); + +public record RegisterUserRequestDto( + [Required] string Username = "", + [Required] string Email = "", + [Required] string Password = "" ); \ No newline at end of file diff --git a/Topers.Core/Models/User.cs b/Topers.Core/Models/User.cs index 30ded8f..81ebc28 100644 --- a/Topers.Core/Models/User.cs +++ b/Topers.Core/Models/User.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; + namespace Topers.Core.Models; /// @@ -5,15 +7,36 @@ namespace Topers.Core.Models; /// public class User { - public User() { } + private User(Guid id, string username, string passwordHash, string email) + { + Id = id; + Username = username; + PasswordHash = passwordHash; + Email = email; + } + + /// + /// Gets or sets a user identifier. + /// + public Guid Id { get; set;} /// - /// Gets a username. + /// Gets a user name. /// - public string Username { get; set; } = string.Empty; + public string Username { get; } = string.Empty; /// - /// Gets a hash password. + /// Gets a user hash password. /// - public string PasswordHash { get; set; } = string.Empty; + public string PasswordHash { get; } = string.Empty; + + /// + /// Gets a user email. + /// + public string Email { get; } = string.Empty; + + public static User Create(Guid id, string username, string passwordHash, string email) + { + return new User(id, username, passwordHash, email); + } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/UserEntity.cs b/Topers.DataAccess.Postgres/Entities/UserEntity.cs new file mode 100644 index 0000000..cbc4ff5 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/UserEntity.cs @@ -0,0 +1,27 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a user entity. +/// +public class UserEntity +{ + /// + /// Gets or sets a user identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets a user name. + /// + public string Username { get; set; } = string.Empty; + + /// + /// Gets or sets a user hash password. + /// + public string PasswordHash { get; set; } = string.Empty; + + /// + /// Gets or sets a user email. + /// + public string Email { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.Designer.cs new file mode 100644 index 0000000..0a5f1f4 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.Designer.cs @@ -0,0 +1,315 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240711155413_AddUserEntity")] + partial class AddUserEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImageName") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Address"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("OrderDetails"); + + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.cs b/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.cs new file mode 100644 index 0000000..c68652c --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240711155413_AddUserEntity.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class AddUserEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Username = table.Column(type: "text", nullable: false), + PasswordHash = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs index 60f9aa3..823ac72 100644 --- a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -197,6 +197,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Orders"); }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => { b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") diff --git a/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs b/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs new file mode 100644 index 0000000..a0866bf --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs @@ -0,0 +1,43 @@ + +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class UsersRepository : IUsersRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public UsersRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Add(User user) + { + var userEntity = new UserEntity() + { + Id = user.Id, + Username = user.Username, + PasswordHash = user.PasswordHash, + Email = user.Email + }; + + await _context.Users.AddAsync(userEntity); + await _context.SaveChangesAsync(); + } + + public async Task GetByName(string username) + { + var userEntity = await _context.Users + .AsNoTracking() + .FirstOrDefaultAsync(u => u.Username == username) ?? throw new Exception(); + + return _mapper.Map(userEntity); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/TopersDbContext.cs b/Topers.DataAccess.Postgres/TopersDbContext.cs index 33f716f..80a6c77 100644 --- a/Topers.DataAccess.Postgres/TopersDbContext.cs +++ b/Topers.DataAccess.Postgres/TopersDbContext.cs @@ -6,6 +6,7 @@ namespace Topers.DataAccess.Postgres; public class TopersDbContext(DbContextOptions options) : DbContext(options) { + public DbSet Users { get; set; } public DbSet Categories { get; set; } public DbSet Goods { get; set; } public DbSet GoodScopes { get; set; } diff --git a/Topers.Infrastructure/Features/JwtOptions.cs b/Topers.Infrastructure/Features/JwtOptions.cs new file mode 100644 index 0000000..8d7acf9 --- /dev/null +++ b/Topers.Infrastructure/Features/JwtOptions.cs @@ -0,0 +1,7 @@ +namespace Topers.Infrastructure.Features; + +public class JwtOptions +{ + public string SecretKey { get; set; } = string.Empty; + public int ExpiresHours { get; set; } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Features/JwtProvider.cs b/Topers.Infrastructure/Features/JwtProvider.cs new file mode 100644 index 0000000..b7aeb92 --- /dev/null +++ b/Topers.Infrastructure/Features/JwtProvider.cs @@ -0,0 +1,33 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Topers.Core.Abstractions; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Features; + +public class JwtProvider(IOptions options) : IJwtProvider +{ + private readonly JwtOptions _options = options.Value; + + public string GenerateToken(User user) + { + var claims = new List() + { + new("userId", user.Id.ToString()) + }; + + var signingCredentials = new SigningCredentials( + new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecretKey)), + SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + claims: claims, + signingCredentials: signingCredentials, + expires: DateTime.UtcNow.AddHours(_options.ExpiresHours)); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Features/PasswordHasher.cs b/Topers.Infrastructure/Features/PasswordHasher.cs new file mode 100644 index 0000000..1fa4c4d --- /dev/null +++ b/Topers.Infrastructure/Features/PasswordHasher.cs @@ -0,0 +1,12 @@ +namespace Topers.Infrastructure.Features; + +using Topers.Core.Abstractions; + +public class PasswordHasher : IPasswordHasher +{ + public string Generate(string password) => + BCrypt.Net.BCrypt.EnhancedHashPassword(password); + + public bool Verify(string password, string hashedPassword) => + BCrypt.Net.BCrypt.EnhancedVerify(password, hashedPassword); +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/UsersService.cs b/Topers.Infrastructure/Services/UsersService.cs new file mode 100644 index 0000000..f3f8e84 --- /dev/null +++ b/Topers.Infrastructure/Services/UsersService.cs @@ -0,0 +1,47 @@ +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class UsersService : IUsersService +{ + private readonly IUsersRepository _usersRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IJwtProvider _jwtProvider; + + public UsersService( + IUsersRepository usersRepository, + IPasswordHasher passwordHasher, + IJwtProvider jwtProvider) + { + _usersRepository = usersRepository; + _passwordHasher = passwordHasher; + _jwtProvider = jwtProvider; + } + + public async Task Register(string username, string email, string password) + { + var hashedPassword = _passwordHasher.Generate(password); + + var newUser = User.Create(Guid.NewGuid(), username, hashedPassword, email); + + await _usersRepository.Add(newUser); + } + + public async Task Login(string username, string password) + { + var user = await _usersRepository.GetByName(username); + + var result = _passwordHasher.Verify(password, user.PasswordHash); + + if (!result) + { + throw new Exception("Failed to log in!"); + } + + var token = _jwtProvider.GenerateToken(user); + + return token; + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Topers.Infrastructure.csproj b/Topers.Infrastructure/Topers.Infrastructure.csproj index 037b891..d0e0168 100644 --- a/Topers.Infrastructure/Topers.Infrastructure.csproj +++ b/Topers.Infrastructure/Topers.Infrastructure.csproj @@ -12,5 +12,7 @@ + + From 15668ce581b8fb9be46862c73219a5ef6bf004a0 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 12 Jul 2024 08:40:13 +0300 Subject: [PATCH 12/39] Fix JWT-token settings bugs --- Topers.Api/Controllers/AccountController.cs | 9 ++++- Topers.Api/Extensions/ApiExtensions.cs | 7 +++- Topers.Api/Program.cs | 39 +++++++++++++++++-- Topers.Api/appsettings.json | 3 ++ .../Features/CookiesOptions.cs | 6 +++ 5 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 Topers.Infrastructure/Features/CookiesOptions.cs diff --git a/Topers.Api/Controllers/AccountController.cs b/Topers.Api/Controllers/AccountController.cs index d717599..a06072c 100644 --- a/Topers.Api/Controllers/AccountController.cs +++ b/Topers.Api/Controllers/AccountController.cs @@ -1,15 +1,20 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Topers.Core.Abstractions; using Topers.Core.Dtos; +using Topers.Infrastructure.Features; namespace Topers.Api.Controllers; [ApiController] [Route("api/account")] -public class AccountController(IUsersService userService) : ControllerBase +public class AccountController( + IUsersService userService, + IOptions options) : ControllerBase { private readonly IUsersService _userService = userService; + private readonly CookiesOptions _cookieOptions = options.Value; [HttpPost("register")] public async Task Register([FromBody] RegisterUserRequestDto request) @@ -24,6 +29,8 @@ public async Task Login([FromBody] LoginUserRequestDto request) { var token = await _userService.Login(request.Username, request.Password); + HttpContext.Response.Cookies.Append(_cookieOptions.Name, token); + return Results.Ok(token); } } \ No newline at end of file diff --git a/Topers.Api/Extensions/ApiExtensions.cs b/Topers.Api/Extensions/ApiExtensions.cs index c6b96c3..1b17c06 100644 --- a/Topers.Api/Extensions/ApiExtensions.cs +++ b/Topers.Api/Extensions/ApiExtensions.cs @@ -12,8 +12,11 @@ public static void AddApiAuthentication( this IServiceCollection services, IOptions jwtOptions) { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 7bd6357..92ae3fd 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -4,18 +4,51 @@ using Topers.DataAccess.Postgres.Repositories; using Topers.Infrastructure.Features; using Topers.Infrastructure.Services; -using Topers.Api.Extensions; -using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Text; var builder = WebApplication.CreateBuilder(args); { var jwtOptionsSection = builder.Configuration.GetSection(nameof(JwtOptions)); + var cookieOptionsSection = builder.Configuration.GetSection(nameof(CookiesOptions)); + var cookieName = cookieOptionsSection.GetValue("Name")!; builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddControllers(); - builder.Services.Configure(builder.Configuration.GetSection(nameof(JwtOptions))); + builder.Services.Configure(jwtOptionsSection); + builder.Services.Configure(cookieOptionsSection); + + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtOptionsSection.GetValue("SecretKey")!)) + }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + context.Token = context.Request.Cookies[cookieName]; + + return Task.CompletedTask; + } + }; + }); + + builder.Services.AddAuthorization(); builder.Services.AddDbContext(options => { diff --git a/Topers.Api/appsettings.json b/Topers.Api/appsettings.json index 1658c5c..fe666cd 100644 --- a/Topers.Api/appsettings.json +++ b/Topers.Api/appsettings.json @@ -12,5 +12,8 @@ "JwtOptions": { "SecretKey": "secretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey", "ExpiresHours": "12" + }, + "CookiesOptions": { + "Name": "topers-app" } } diff --git a/Topers.Infrastructure/Features/CookiesOptions.cs b/Topers.Infrastructure/Features/CookiesOptions.cs new file mode 100644 index 0000000..7c0dd27 --- /dev/null +++ b/Topers.Infrastructure/Features/CookiesOptions.cs @@ -0,0 +1,6 @@ +namespace Topers.Infrastructure.Features; + +public class CookiesOptions +{ + public string Name { get; set; } = string.Empty; +} \ No newline at end of file From 59b248a1ef6f2d9981c2e71415150aed24df2313 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 12 Jul 2024 08:44:35 +0300 Subject: [PATCH 13/39] Add cookies security settings --- Topers.Api/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 92ae3fd..6b09fc2 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -7,6 +7,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Text; +using Microsoft.AspNetCore.CookiePolicy; var builder = WebApplication.CreateBuilder(args); { @@ -87,6 +88,12 @@ } app.UseHttpsRedirection(); + app.UseCookiePolicy(new CookiePolicyOptions + { + MinimumSameSitePolicy = SameSiteMode.Strict, + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always + }); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); From 5c11b19eda65d1128771fe6456b973766b83597d Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 12 Jul 2024 09:05:16 +0300 Subject: [PATCH 14/39] Fix extension bugs --- Topers.Api/Extensions/ApiExtensions.cs | 34 -------------------------- 1 file changed, 34 deletions(-) delete mode 100644 Topers.Api/Extensions/ApiExtensions.cs diff --git a/Topers.Api/Extensions/ApiExtensions.cs b/Topers.Api/Extensions/ApiExtensions.cs deleted file mode 100644 index 1b17c06..0000000 --- a/Topers.Api/Extensions/ApiExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace Topers.Api.Extensions; - -using System.Text; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.Extensions.Options; -using Microsoft.IdentityModel.Tokens; -using Topers.Infrastructure.Features; - -public static class ApiExtensions -{ - public static void AddApiAuthentication( - this IServiceCollection services, - IOptions jwtOptions) - { - services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(jwtOptions.Value.SecretKey)) - }; - }); - - services.AddAuthorization(); - } -} \ No newline at end of file From 8787bf4f8a269b0662cb6fd0aac9f20b2e01b5bc Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 12 Jul 2024 15:40:31 +0300 Subject: [PATCH 15/39] Update Good Entity, Good Dtos and Good Model. Add new migration --- Topers.Api/Controllers/GoodsController.cs | 8 +- Topers.Core/Dtos/GoodDto.cs | 8 +- Topers.Core/Dtos/GoodScopeDto.cs | 9 +- Topers.Core/Models/Good.cs | 14 +- Topers.Core/Topers.Core.csproj | 4 + .../Entities/GoodEntity.cs | 14 - .../Entities/GoodScopeEntity.cs | 5 + ...712123931_UpdateGoodImageField.Designer.cs | 315 ++++++++++++++++++ .../20240712123931_UpdateGoodImageField.cs | 38 +++ .../TopersDbContextModelSnapshot.cs | 6 +- .../Repositories/GoodsRepository.cs | 8 +- 11 files changed, 379 insertions(+), 50 deletions(-) create mode 100644 Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.cs diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index 574b14c..d9d0c79 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -62,9 +62,7 @@ public async Task> CreateGood([FromBody] GoodReque Guid.Empty, good.CategoryId, good.Name, - good.Description, - good.ImageName, - good.Image + good.Description ); await _goodService.CreateGoodAsync(newGood); @@ -91,9 +89,7 @@ public async Task> UpdateGood([FromRoute] Guid goo goodId, good.CategoryId, good.Name, - good.Description, - good.ImageName, - good.Image + good.Description ); var updatedGood = await _goodService.UpdateGoodAsync(existGood); diff --git a/Topers.Core/Dtos/GoodDto.cs b/Topers.Core/Dtos/GoodDto.cs index d3f133f..42284b3 100644 --- a/Topers.Core/Dtos/GoodDto.cs +++ b/Topers.Core/Dtos/GoodDto.cs @@ -5,15 +5,11 @@ namespace Topers.Core.Dtos; public record GoodResponseDto( Guid Id, string Name = "", - string Description = "", - string ImageName = "", - IFormFile? Image = null + string Description = "" ); public record GoodRequestDto( Guid CategoryId, string Name = "", - string Description = "", - string ImageName = "", - IFormFile? Image = null + string Description = "" ); \ No newline at end of file diff --git a/Topers.Core/Dtos/GoodScopeDto.cs b/Topers.Core/Dtos/GoodScopeDto.cs index 29750dd..2539dec 100644 --- a/Topers.Core/Dtos/GoodScopeDto.cs +++ b/Topers.Core/Dtos/GoodScopeDto.cs @@ -1,14 +1,19 @@ +using Microsoft.AspNetCore.Http; + namespace Topers.Core.Dtos; public record GoodScopeResponseDto( Guid Id, Guid GoodId, int Litre, - decimal Price + decimal Price, + string? ImageName = "", + IFormFile? Image = null ); public record GoodScopeRequestDto( Guid GoodId, int Litre, - decimal Price + decimal Price, + IFormFile? Image = null ); \ No newline at end of file diff --git a/Topers.Core/Models/Good.cs b/Topers.Core/Models/Good.cs index 95f9025..daad6d3 100644 --- a/Topers.Core/Models/Good.cs +++ b/Topers.Core/Models/Good.cs @@ -7,14 +7,12 @@ namespace Topers.Core.Models; /// public class Good { - public Good(Guid id, Guid categoryId, string name, string description, string imageName, IFormFile? image) + public Good(Guid id, Guid categoryId, string name, string description) { Id = id; CategoryId = categoryId; Name = name; Description = description; - ImageName = imageName; - Image = image; } /// @@ -36,14 +34,4 @@ public Good(Guid id, Guid categoryId, string name, string description, string im /// Gets or sets a good description. /// public string? Description { get; } = string.Empty; - - /// - /// Gets or sets a good image name file. - /// - public string? ImageName { get; } = string.Empty; - - /// - /// Gets or sets a good image. - /// - public IFormFile? Image { get; } } \ No newline at end of file diff --git a/Topers.Core/Topers.Core.csproj b/Topers.Core/Topers.Core.csproj index d7650b9..341ab4a 100644 --- a/Topers.Core/Topers.Core.csproj +++ b/Topers.Core/Topers.Core.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs index 842d073..1b8482e 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs @@ -1,8 +1,5 @@ namespace Topers.DataAccess.Postgres.Entities; -using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.AspNetCore.Http; - /// /// Represents a good entity. /// @@ -23,17 +20,6 @@ public class GoodEntity /// public string? Description { get; set; } - /// - /// Gets or sets a good image name file. - /// - public string? ImageName { get; set; } = string.Empty; - - [NotMapped] - /// - /// Gets or sets a good image. - /// - public IFormFile? Image { get; set; } - /// /// Gets or sets a good category identifier. /// diff --git a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs index 04155b6..cd72813 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs @@ -29,4 +29,9 @@ public class GoodScopeEntity /// Gets or sets the price. /// public decimal Price { get; set; } + + /// + /// Gets or sets a good image. + /// + public string? Image { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.Designer.cs new file mode 100644 index 0000000..d521402 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.Designer.cs @@ -0,0 +1,315 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240712123931_UpdateGoodImageField")] + partial class UpdateGoodImageField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Address"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("OrderDetails"); + + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.cs b/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.cs new file mode 100644 index 0000000..609cf16 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240712123931_UpdateGoodImageField.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class UpdateGoodImageField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ImageName", + table: "Goods"); + + migrationBuilder.AddColumn( + name: "Image", + table: "GoodScopes", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Image", + table: "GoodScopes"); + + migrationBuilder.AddColumn( + name: "ImageName", + table: "Goods", + type: "text", + nullable: true); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs index 823ac72..120938d 100644 --- a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -112,9 +112,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("text"); - b.Property("ImageName") - .HasColumnType("text"); - b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -135,6 +132,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("GoodId") .HasColumnType("uuid"); + b.Property("Image") + .HasColumnType("text"); + b.Property("Litre") .HasColumnType("integer"); diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index d79601b..b9f3918 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -24,9 +24,7 @@ public async Task CreateAsync(Good good) Id = Guid.NewGuid(), CategoryId = good.CategoryId, Name = good.Name, - Description = good.Description, - ImageName = good.ImageName, - Image = good.Image + Description = good.Description }; await _context.Goods.AddAsync(goodEntity); @@ -87,9 +85,7 @@ await _context.Goods .ExecuteUpdateAsync(s => s .SetProperty(g => g.Name, good.Name) .SetProperty(g => g.CategoryId, good.CategoryId) - .SetProperty(g => g.Description, good.Description) - .SetProperty(g => g.ImageName, good.ImageName) - .SetProperty(g => g.Image, good.Image)); + .SetProperty(g => g.Description, good.Description)); return good.Id; } From 797f5d2e86ae8ac79a5775d80ccf4fd5a8ca7e89 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 12 Jul 2024 21:12:44 +0300 Subject: [PATCH 16/39] Implement IFileService. Set up uploading and viewing a Good image --- Topers.Api/Controllers/GoodsController.cs | 50 ++++++++++++-- Topers.Api/Mapping/MappingProfile.cs | 26 +++++++- Topers.Api/Program.cs | 9 +++ Topers.Api/Topers.Api.csproj | 1 + Topers.Core/Abstractions/IFileService.cs | 10 +++ Topers.Core/Abstractions/IGoodsRepository.cs | 1 + Topers.Core/Abstractions/IGoodsService.cs | 3 +- Topers.Core/Dtos/GoodDto.cs | 5 +- Topers.Core/Dtos/GoodScopeDto.cs | 7 +- Topers.Core/Models/Good.cs | 7 ++ Topers.Core/Models/GoodScope.cs | 8 ++- .../Validators/GoodScopeDtoValidator.cs | 3 - .../Entities/GoodScopeEntity.cs | 9 +++ .../Repositories/GoodsRepository.cs | 17 +++++ Topers.Infrastructure/Features/FileService.cs | 66 +++++++++++++++++++ .../Services/GoodsService.cs | 19 +++++- 16 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 Topers.Core/Abstractions/IFileService.cs create mode 100644 Topers.Infrastructure/Features/FileService.cs diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index d9d0c79..87abc07 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -1,17 +1,23 @@ namespace Topers.Api.Contollers; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Swashbuckle.AspNetCore.Annotations; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; using Topers.Core.Validators; +using Topers.DataAccess.Postgres.Entities; +using Topers.Infrastructure.Features; [ApiController] [Route("api/goods")] -public class GoodsController(IGoodsService goodService) : ControllerBase +public class GoodsController( + IGoodsService goodService, + IFileService fileService) : ControllerBase { private readonly IGoodsService _goodService = goodService; + private readonly IFileService _fileService = fileService; [HttpGet] [SwaggerResponse(200, Description = "Returns a good list.", Type = typeof(IEnumerable))] @@ -49,7 +55,7 @@ public async Task> GetGood([FromRoute] Guid goodId public async Task> CreateGood([FromBody] GoodRequestDto good) { var newGoodValidator = new GoodDtoValidator(); - + var newGoodValidatorResult = newGoodValidator.Validate(good); if (!newGoodValidatorResult.IsValid) @@ -65,9 +71,9 @@ public async Task> CreateGood([FromBody] GoodReque good.Description ); - await _goodService.CreateGoodAsync(newGood); + var newGoodEntity = await _goodService.CreateGoodAsync(newGood); - return Ok(newGood); + return Ok(newGoodEntity); } [HttpPut("{goodId:guid}")] @@ -76,7 +82,7 @@ public async Task> CreateGood([FromBody] GoodReque public async Task> UpdateGood([FromRoute] Guid goodId, [FromBody] GoodRequestDto good) { var goodValidator = new GoodDtoValidator(); - + var goodValidatorResult = goodValidator.Validate(good); if (!goodValidatorResult.IsValid) @@ -105,4 +111,38 @@ public async Task> DeleteGood([FromRoute] Guid goodId) return Ok(goodId); } + + [HttpPost("{goodId:guid}/addScope")] + public async Task> AddGoodScope([FromRoute] Guid goodId, [FromForm] GoodScopeRequestDto scope) + { + if (scope.Image == null) + { + return BadRequest("Good image not found!"); + } + + Guid imageGuid = Guid.Empty; + + var fileResult = _fileService.SaveImage(scope.Image); + + if (fileResult.Item1 == 1) + { + var imageNameWithoutExtension = Path.GetFileNameWithoutExtension(fileResult.Item2); + + imageGuid = Guid.Parse(imageNameWithoutExtension); + + scope = scope with { ImageName = fileResult.Item2 }; + } + + var scopeModel = new GoodScope( + Guid.Empty, + goodId, + scope.Litre, + scope.Price, + scope.ImageName + ); + + await _goodService.AddGoodScopeAsync(scopeModel); + + return Ok(imageGuid); + } } \ No newline at end of file diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index e33b4b8..3c3299d 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -15,7 +15,7 @@ public MappingProfile() CreateMap(); CreateMap() - .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address != null ? new AddressResponseDto ( + .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address != null ? new AddressResponseDto( src.Address.Id, src.Id, src.Address.Street, @@ -29,8 +29,28 @@ public MappingProfile() CreateMap(); CreateMap(); - CreateMap(); - CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + CreateMap() + .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + + CreateMap() + .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => + src.Scopes != null + ? src.Scopes.Select(scope => new GoodScopeResponseDto( + scope.Id, + scope.GoodId, + scope.Litre, + scope.Price, + scope.Image, + null + )).ToList() + : new List())); + CreateMap(); CreateMap(); diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 6b09fc2..166c677 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using System.Text; using Microsoft.AspNetCore.CookiePolicy; +using Microsoft.Extensions.FileProviders; var builder = WebApplication.CreateBuilder(args); { @@ -72,6 +73,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); }; var app = builder.Build(); @@ -88,6 +90,13 @@ } app.UseHttpsRedirection(); + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider( + Path.Combine(builder.Environment.ContentRootPath, "Uploads") + ), + RequestPath = "/Resources" + }); app.UseCookiePolicy(new CookiePolicyOptions { MinimumSameSitePolicy = SameSiteMode.Strict, diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index 95069fc..960dcb3 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -17,6 +17,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Topers.Core/Abstractions/IFileService.cs b/Topers.Core/Abstractions/IFileService.cs new file mode 100644 index 0000000..da674dc --- /dev/null +++ b/Topers.Core/Abstractions/IFileService.cs @@ -0,0 +1,10 @@ + +using Microsoft.AspNetCore.Http; + +namespace Topers.Core.Abstractions; + +public interface IFileService +{ + Tuple SaveImage(IFormFile imageFile); + void DeleteImage(string fullFileName); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsRepository.cs b/Topers.Core/Abstractions/IGoodsRepository.cs index 23c8d96..98c5b5d 100644 --- a/Topers.Core/Abstractions/IGoodsRepository.cs +++ b/Topers.Core/Abstractions/IGoodsRepository.cs @@ -10,4 +10,5 @@ public interface IGoodsRepository Task> GetByFilterAsync(string title); Task UpdateAsync(Good good); Task DeleteAsync(Guid goodId); + Task AddScopeAsync(GoodScope goodScope); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsService.cs b/Topers.Core/Abstractions/IGoodsService.cs index 44a131d..3081642 100644 --- a/Topers.Core/Abstractions/IGoodsService.cs +++ b/Topers.Core/Abstractions/IGoodsService.cs @@ -5,9 +5,10 @@ namespace Topers.Core.Abstractions; public interface IGoodsService { - Task CreateGoodAsync(Good good); + Task CreateGoodAsync(Good good); Task> GetAllGoodsAsync(); Task GetGoodByIdAsync(Guid goodId); Task UpdateGoodAsync(Good good); Task DeleteGoodAsync(Guid goodId); + Task AddGoodScopeAsync(GoodScope scope); }; \ No newline at end of file diff --git a/Topers.Core/Dtos/GoodDto.cs b/Topers.Core/Dtos/GoodDto.cs index 42284b3..e4c7178 100644 --- a/Topers.Core/Dtos/GoodDto.cs +++ b/Topers.Core/Dtos/GoodDto.cs @@ -1,11 +1,10 @@ -using Microsoft.AspNetCore.Http; - namespace Topers.Core.Dtos; public record GoodResponseDto( Guid Id, string Name = "", - string Description = "" + string? Description = "", + List? Scopes = null ); public record GoodRequestDto( diff --git a/Topers.Core/Dtos/GoodScopeDto.cs b/Topers.Core/Dtos/GoodScopeDto.cs index 2539dec..c15b7dc 100644 --- a/Topers.Core/Dtos/GoodScopeDto.cs +++ b/Topers.Core/Dtos/GoodScopeDto.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Http; namespace Topers.Core.Dtos; @@ -12,8 +13,8 @@ public record GoodScopeResponseDto( ); public record GoodScopeRequestDto( - Guid GoodId, - int Litre, - decimal Price, + [Required] int Litre, + [Required] decimal Price, + string? ImageName = "", IFormFile? Image = null ); \ No newline at end of file diff --git a/Topers.Core/Models/Good.cs b/Topers.Core/Models/Good.cs index daad6d3..049cd56 100644 --- a/Topers.Core/Models/Good.cs +++ b/Topers.Core/Models/Good.cs @@ -1,5 +1,6 @@ namespace Topers.Core.Models; +using System.Collections; using Microsoft.AspNetCore.Http; /// @@ -13,6 +14,7 @@ public Good(Guid id, Guid categoryId, string name, string description) CategoryId = categoryId; Name = name; Description = description; + Scopes = []; } /// @@ -34,4 +36,9 @@ public Good(Guid id, Guid categoryId, string name, string description) /// Gets or sets a good description. /// public string? Description { get; } = string.Empty; + + /// + /// Gets or sets a good scopes. + /// + public ICollection? Scopes { get; } } \ No newline at end of file diff --git a/Topers.Core/Models/GoodScope.cs b/Topers.Core/Models/GoodScope.cs index 901840d..eca0ce0 100644 --- a/Topers.Core/Models/GoodScope.cs +++ b/Topers.Core/Models/GoodScope.cs @@ -5,12 +5,13 @@ namespace Topers.Core.Models; /// public class GoodScope { - public GoodScope(Guid id, Guid goodId, int litre, decimal price) + public GoodScope(Guid id, Guid goodId, int litre, decimal price, string? image) { Id = id; GoodId = goodId; Litre = litre; Price = price; + Image = image; } /// @@ -32,4 +33,9 @@ public GoodScope(Guid id, Guid goodId, int litre, decimal price) /// Gets or sets the price. /// public decimal Price { get; } + + /// + /// Gets or sets the image. + /// + public string? Image { get; } } \ No newline at end of file diff --git a/Topers.Core/Validators/GoodScopeDtoValidator.cs b/Topers.Core/Validators/GoodScopeDtoValidator.cs index 82c237d..9b3235c 100644 --- a/Topers.Core/Validators/GoodScopeDtoValidator.cs +++ b/Topers.Core/Validators/GoodScopeDtoValidator.cs @@ -7,9 +7,6 @@ public class GoodScopeDtoValidator : AbstractValidator { public GoodScopeDtoValidator() { - RuleFor(g => g.GoodId) - .NotEmpty().WithMessage("{PropertyName} is required!") - .NotNull(); RuleFor(g => g.Litre) .Must(IsLitreValid).WithMessage("Litre must be one of the following values: 1, 5, 10, 15, 25, 1000!"); RuleFor(g => g.Price) diff --git a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs index cd72813..e555853 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs @@ -1,3 +1,6 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.AspNetCore.Http; + namespace Topers.DataAccess.Postgres.Entities; /// @@ -34,4 +37,10 @@ public class GoodScopeEntity /// Gets or sets a good image. /// public string? Image { get; set; } = string.Empty; + + [NotMapped] + /// + /// Gets or sets a good image file. + /// + public IFormFile? ImageFile { get; set; } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index b9f3918..f46b23b 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -89,4 +89,21 @@ await _context.Goods return good.Id; } + + public async Task AddScopeAsync(GoodScope goodScope) + { + var scopeEntity = new GoodScopeEntity + { + Id = Guid.NewGuid(), + GoodId = goodScope.GoodId, + Litre = goodScope.Litre, + Price = goodScope.Price, + Image = goodScope.Image + }; + + await _context.GoodScopes.AddAsync(scopeEntity); + await _context.SaveChangesAsync(); + + return scopeEntity.Id; + } } \ No newline at end of file diff --git a/Topers.Infrastructure/Features/FileService.cs b/Topers.Infrastructure/Features/FileService.cs new file mode 100644 index 0000000..bdab7ee --- /dev/null +++ b/Topers.Infrastructure/Features/FileService.cs @@ -0,0 +1,66 @@ +namespace Topers.Infrastructure.Features; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Topers.Core.Abstractions; + +public class FileService : IFileService +{ + private readonly IWebHostEnvironment _environment; + + public FileService(IWebHostEnvironment environment) + { + _environment = environment; + } + + public void DeleteImage(string fullFileName) + { + var contentPath = _environment.ContentRootPath; + var path = Path.Combine(contentPath, $"Uploads", fullFileName); + + if (File.Exists(path)) + { + File.Delete(path); + } + } + + public Tuple SaveImage(IFormFile imageFile) + { + try + { + var contentPath = _environment.ContentRootPath; + + var path = Path.Combine(contentPath, "Uploads"); + + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + var ext = Path.GetExtension(imageFile.FileName); + var allowedExtensions = new string[] { ".jpg", ".png", ".jpeg" }; + + if (!allowedExtensions.Contains(ext)) + { + string msg = string.Format("Only {0} extensions are allowed", string.Join(",", allowedExtensions)); + return new Tuple(0, msg); + } + + string uniqueString = Guid.NewGuid().ToString(); + + var newFileName = uniqueString + ext; + var fileWithPath = Path.Combine(path, newFileName); + var stream = new FileStream(fileWithPath, FileMode.Create); + + imageFile.CopyTo(stream); + + stream.Close(); + + return new Tuple(1, newFileName); + } + catch (Exception) + { + return new Tuple(0, "Error has occured"); + } + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/GoodsService.cs b/Topers.Infrastructure/Services/GoodsService.cs index 4c6cf22..f7d4225 100644 --- a/Topers.Infrastructure/Services/GoodsService.cs +++ b/Topers.Infrastructure/Services/GoodsService.cs @@ -16,9 +16,19 @@ public GoodsService(IGoodsRepository goodsRepository, IMapper mapper) _mapper = mapper; } - public async Task CreateGoodAsync(Good good) + public async Task CreateGoodAsync(Good good) { - return await _goodsRepository.CreateAsync(good); + var newGoodIdentifier = await _goodsRepository.CreateAsync(good); + + var newGood = new GoodResponseDto + ( + newGoodIdentifier, + good.Name, + good.Description, + null + ); + + return newGood; } public async Task DeleteGoodAsync(Guid goodId) @@ -40,4 +50,9 @@ public async Task UpdateGoodAsync(Good good) { return await _goodsRepository.UpdateAsync(good); } + + public async Task AddGoodScopeAsync(GoodScope scope) + { + return await _goodsRepository.AddScopeAsync(scope); + } } \ No newline at end of file From d99b20f4a18eb6f04f87175e7aa5de0fabd26154 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 14 Jul 2024 20:56:07 +0300 Subject: [PATCH 17/39] Fix correct Good preview --- Topers.Api/Controllers/GoodsController.cs | 14 ++++---------- Topers.Api/Mapping/MappingProfile.cs | 9 +++++++-- Topers.Core/Dtos/CustomerDto.cs | 2 ++ Topers.Core/Dtos/GoodScopeDto.cs | 13 ++++++------- Topers.Core/Models/Good.cs | 2 +- Topers.Core/Models/GoodScope.cs | 4 ++-- .../Repositories/GoodsRepository.cs | 3 ++- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index 87abc07..78db3a7 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -115,21 +115,15 @@ public async Task> DeleteGood([FromRoute] Guid goodId) [HttpPost("{goodId:guid}/addScope")] public async Task> AddGoodScope([FromRoute] Guid goodId, [FromForm] GoodScopeRequestDto scope) { - if (scope.Image == null) + if (scope.ImageFile == null) { return BadRequest("Good image not found!"); } - Guid imageGuid = Guid.Empty; - - var fileResult = _fileService.SaveImage(scope.Image); + var fileResult = _fileService.SaveImage(scope.ImageFile); if (fileResult.Item1 == 1) { - var imageNameWithoutExtension = Path.GetFileNameWithoutExtension(fileResult.Item2); - - imageGuid = Guid.Parse(imageNameWithoutExtension); - scope = scope with { ImageName = fileResult.Item2 }; } @@ -141,8 +135,8 @@ public async Task> AddGoodScope([FromRoute] Guid goodId, [Fro scope.ImageName ); - await _goodService.AddGoodScopeAsync(scopeModel); + var newScopeIdentifier = await _goodService.AddGoodScopeAsync(scopeModel); - return Ok(imageGuid); + return Ok(newScopeIdentifier); } } \ No newline at end of file diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index 3c3299d..ab06129 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -38,6 +38,12 @@ public MappingProfile() CreateMap() .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + CreateMap() + .ForMember(dest => dest.ImageName, opt => opt.MapFrom(src => src.ImageName)); + + CreateMap() + .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + CreateMap() .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null @@ -46,8 +52,7 @@ public MappingProfile() scope.GoodId, scope.Litre, scope.Price, - scope.Image, - null + scope.ImageName )).ToList() : new List())); diff --git a/Topers.Core/Dtos/CustomerDto.cs b/Topers.Core/Dtos/CustomerDto.cs index 7848773..05d21dd 100644 --- a/Topers.Core/Dtos/CustomerDto.cs +++ b/Topers.Core/Dtos/CustomerDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace Topers.Core.Dtos; public record CustomerResponseDto( diff --git a/Topers.Core/Dtos/GoodScopeDto.cs b/Topers.Core/Dtos/GoodScopeDto.cs index c15b7dc..3a965fb 100644 --- a/Topers.Core/Dtos/GoodScopeDto.cs +++ b/Topers.Core/Dtos/GoodScopeDto.cs @@ -4,17 +4,16 @@ namespace Topers.Core.Dtos; public record GoodScopeResponseDto( - Guid Id, - Guid GoodId, - int Litre, - decimal Price, - string? ImageName = "", - IFormFile? Image = null + Guid Id = default, + Guid GoodId = default, + int Litre = default, + decimal Price = default, + string? ImageName = "" ); public record GoodScopeRequestDto( [Required] int Litre, [Required] decimal Price, string? ImageName = "", - IFormFile? Image = null + IFormFile? ImageFile = null ); \ No newline at end of file diff --git a/Topers.Core/Models/Good.cs b/Topers.Core/Models/Good.cs index 049cd56..b0e43fb 100644 --- a/Topers.Core/Models/Good.cs +++ b/Topers.Core/Models/Good.cs @@ -14,7 +14,7 @@ public Good(Guid id, Guid categoryId, string name, string description) CategoryId = categoryId; Name = name; Description = description; - Scopes = []; + Scopes = new List(); } /// diff --git a/Topers.Core/Models/GoodScope.cs b/Topers.Core/Models/GoodScope.cs index eca0ce0..15a14b6 100644 --- a/Topers.Core/Models/GoodScope.cs +++ b/Topers.Core/Models/GoodScope.cs @@ -11,7 +11,7 @@ public GoodScope(Guid id, Guid goodId, int litre, decimal price, string? image) GoodId = goodId; Litre = litre; Price = price; - Image = image; + ImageName = image; } /// @@ -37,5 +37,5 @@ public GoodScope(Guid id, Guid goodId, int litre, decimal price, string? image) /// /// Gets or sets the image. /// - public string? Image { get; } + public string? ImageName { get; } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index f46b23b..cd395e4 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -45,6 +45,7 @@ await _context.Goods public async Task> GetAllAsync() { var goodEntities = await _context.Goods + .Include(g => g.Scopes) .AsNoTracking() .ToListAsync(); @@ -98,7 +99,7 @@ public async Task AddScopeAsync(GoodScope goodScope) GoodId = goodScope.GoodId, Litre = goodScope.Litre, Price = goodScope.Price, - Image = goodScope.Image + Image = goodScope.ImageName }; await _context.GoodScopes.AddAsync(scopeEntity); From 0c5371d3ef3ee8e7e9fbfa93114fa09ae4a9d3e1 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 15 Jul 2024 09:12:24 +0300 Subject: [PATCH 18/39] Update logic of AddScope method in GoodsController. --- Topers.Api/Controllers/GoodsController.cs | 5 ++++- Topers.Core/Abstractions/IGoodsRepository.cs | 2 ++ Topers.Core/Abstractions/IGoodsService.cs | 2 ++ .../Repositories/GoodsRepository.cs | 20 +++++++++++++++++++ Topers.Infrastructure/Features/FileService.cs | 2 -- .../Services/GoodsService.cs | 12 +++++++++++ 6 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index 78db3a7..92876ab 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -127,6 +127,7 @@ public async Task> AddGoodScope([FromRoute] Guid goodId, [Fro scope = scope with { ImageName = fileResult.Item2 }; } + var scopeModel = new GoodScope( Guid.Empty, goodId, @@ -135,7 +136,9 @@ public async Task> AddGoodScope([FromRoute] Guid goodId, [Fro scope.ImageName ); - var newScopeIdentifier = await _goodService.AddGoodScopeAsync(scopeModel); + var isUpdated = await _goodService.IsGoodScopeExistsAsync(goodId, scopeModel.Litre); + + var newScopeIdentifier = (!isUpdated) ? await _goodService.AddGoodScopeAsync(scopeModel) : await _goodService.UpdateGoodScopeAsync(scopeModel); return Ok(newScopeIdentifier); } diff --git a/Topers.Core/Abstractions/IGoodsRepository.cs b/Topers.Core/Abstractions/IGoodsRepository.cs index 98c5b5d..c1c3c39 100644 --- a/Topers.Core/Abstractions/IGoodsRepository.cs +++ b/Topers.Core/Abstractions/IGoodsRepository.cs @@ -10,5 +10,7 @@ public interface IGoodsRepository Task> GetByFilterAsync(string title); Task UpdateAsync(Good good); Task DeleteAsync(Guid goodId); + Task GetScopeAsync(Guid goodId, int litre); Task AddScopeAsync(GoodScope goodScope); + Task UpdateScopeAsync(GoodScope goodScope); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsService.cs b/Topers.Core/Abstractions/IGoodsService.cs index 3081642..0e898aa 100644 --- a/Topers.Core/Abstractions/IGoodsService.cs +++ b/Topers.Core/Abstractions/IGoodsService.cs @@ -11,4 +11,6 @@ public interface IGoodsService Task UpdateGoodAsync(Good good); Task DeleteGoodAsync(Guid goodId); Task AddGoodScopeAsync(GoodScope scope); + Task UpdateGoodScopeAsync(GoodScope scope); + Task IsGoodScopeExistsAsync(Guid goodId, int litre); }; \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index cd395e4..5f32cc1 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -91,6 +91,14 @@ await _context.Goods return good.Id; } + public async Task GetScopeAsync(Guid goodId, int litre) + { + var pExistsGoodScope = await _context.GoodScopes + .FirstOrDefaultAsync(gs => gs.GoodId == goodId && gs.Litre == litre); + + return _mapper.Map(pExistsGoodScope); + } + public async Task AddScopeAsync(GoodScope goodScope) { var scopeEntity = new GoodScopeEntity @@ -107,4 +115,16 @@ public async Task AddScopeAsync(GoodScope goodScope) return scopeEntity.Id; } + + public async Task UpdateScopeAsync(GoodScope goodScope) + { + await _context.GoodScopes + .Where(gs => gs.GoodId == goodScope.GoodId) + .ExecuteUpdateAsync(s => s + .SetProperty(gs => gs.Image, goodScope.ImageName) + .SetProperty(gs => gs.Litre, goodScope.Litre) + .SetProperty(gs => gs.Price, goodScope.Price)); + + return goodScope.Id; + } } \ No newline at end of file diff --git a/Topers.Infrastructure/Features/FileService.cs b/Topers.Infrastructure/Features/FileService.cs index bdab7ee..ae6ec95 100644 --- a/Topers.Infrastructure/Features/FileService.cs +++ b/Topers.Infrastructure/Features/FileService.cs @@ -51,9 +51,7 @@ public Tuple SaveImage(IFormFile imageFile) var newFileName = uniqueString + ext; var fileWithPath = Path.Combine(path, newFileName); var stream = new FileStream(fileWithPath, FileMode.Create); - imageFile.CopyTo(stream); - stream.Close(); return new Tuple(1, newFileName); diff --git a/Topers.Infrastructure/Services/GoodsService.cs b/Topers.Infrastructure/Services/GoodsService.cs index f7d4225..c4637c5 100644 --- a/Topers.Infrastructure/Services/GoodsService.cs +++ b/Topers.Infrastructure/Services/GoodsService.cs @@ -55,4 +55,16 @@ public async Task AddGoodScopeAsync(GoodScope scope) { return await _goodsRepository.AddScopeAsync(scope); } + + public async Task UpdateGoodScopeAsync(GoodScope scope) + { + return await _goodsRepository.UpdateScopeAsync(scope); + } + + public async Task IsGoodScopeExistsAsync(Guid goodId, int litre) + { + var existingScope = await _goodsRepository.GetScopeAsync(goodId, litre); + + return existingScope != null; + } } \ No newline at end of file From bf9ca4642769926d1accc247d599dd2fafae32bc Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 15 Jul 2024 09:40:07 +0300 Subject: [PATCH 19/39] Update correct GoodScope identifier preview --- Topers.Api/Controllers/GoodsController.cs | 4 ---- Topers.Api/Program.cs | 11 +++++++++++ Topers.Api/Topers.Api.http | 6 ------ .../Repositories/GoodsRepository.cs | 8 ++++++-- 4 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 Topers.Api/Topers.Api.http diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index 92876ab..bb55ce4 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -1,14 +1,11 @@ namespace Topers.Api.Contollers; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore.Metadata.Internal; using Swashbuckle.AspNetCore.Annotations; using Topers.Core.Abstractions; using Topers.Core.Dtos; using Topers.Core.Models; using Topers.Core.Validators; -using Topers.DataAccess.Postgres.Entities; -using Topers.Infrastructure.Features; [ApiController] [Route("api/goods")] @@ -127,7 +124,6 @@ public async Task> AddGoodScope([FromRoute] Guid goodId, [Fro scope = scope with { ImageName = fileResult.Item2 }; } - var scopeModel = new GoodScope( Guid.Empty, goodId, diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 166c677..38d5f86 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -9,6 +9,8 @@ using System.Text; using Microsoft.AspNetCore.CookiePolicy; using Microsoft.Extensions.FileProviders; +using System.Globalization; +using Microsoft.AspNetCore.Localization; var builder = WebApplication.CreateBuilder(args); { @@ -76,6 +78,14 @@ builder.Services.AddScoped(); }; +var defaultCulture = new CultureInfo("en-US"); +var localizationOptions = new RequestLocalizationOptions +{ + DefaultRequestCulture = new RequestCulture(defaultCulture), + SupportedCultures = [defaultCulture], + SupportedUICultures = [defaultCulture] +}; + var app = builder.Build(); { if (app.Environment.IsDevelopment()) @@ -90,6 +100,7 @@ } app.UseHttpsRedirection(); + app.UseRequestLocalization(localizationOptions); app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider( diff --git a/Topers.Api/Topers.Api.http b/Topers.Api/Topers.Api.http deleted file mode 100644 index df31883..0000000 --- a/Topers.Api/Topers.Api.http +++ /dev/null @@ -1,6 +0,0 @@ -@Topers.Api_HostAddress = http://localhost:5264 - -GET {{Topers.Api_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index 5f32cc1..2987bc2 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -118,13 +118,17 @@ public async Task AddScopeAsync(GoodScope goodScope) public async Task UpdateScopeAsync(GoodScope goodScope) { + var existingGoodScope = await _context.GoodScopes + .AsNoTracking() + .FirstOrDefaultAsync(gs => gs.GoodId == goodScope.GoodId && gs.Litre == goodScope.Litre); + await _context.GoodScopes - .Where(gs => gs.GoodId == goodScope.GoodId) + .Where(gs => gs.GoodId == goodScope.GoodId && gs.Litre == goodScope.Litre) .ExecuteUpdateAsync(s => s .SetProperty(gs => gs.Image, goodScope.ImageName) .SetProperty(gs => gs.Litre, goodScope.Litre) .SetProperty(gs => gs.Price, goodScope.Price)); - return goodScope.Id; + return existingGoodScope!.Id; } } \ No newline at end of file From f1d3c23d87da67c156c5508942c137f4777ddc44 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Wed, 17 Jul 2024 12:27:56 +0300 Subject: [PATCH 20/39] Implement Feature-Sliced Design in client-side --- Topers.Client/index.html | 4 +- Topers.Client/package-lock.json | 2307 ++++++++++++++++++-- Topers.Client/package.json | 8 +- Topers.Client/src/App.jsx | 9 - Topers.Client/src/app/images/logo.svg | 54 + Topers.Client/src/app/images/site_logo.svg | 49 + Topers.Client/src/app/styles/Header.css | 11 + Topers.Client/src/app/styles/index.css | 13 + Topers.Client/src/entities/Good.ts | 5 + Topers.Client/src/index.css | 0 Topers.Client/src/main.jsx | 4 +- Topers.Client/src/pages/MainPage.jsx | 16 + Topers.Client/src/shared/Cart.jsx | 37 + Topers.Client/src/widgets/Header.jsx | 47 + 14 files changed, 2347 insertions(+), 217 deletions(-) delete mode 100644 Topers.Client/src/App.jsx create mode 100644 Topers.Client/src/app/images/logo.svg create mode 100644 Topers.Client/src/app/images/site_logo.svg create mode 100644 Topers.Client/src/app/styles/Header.css create mode 100644 Topers.Client/src/app/styles/index.css create mode 100644 Topers.Client/src/entities/Good.ts delete mode 100644 Topers.Client/src/index.css create mode 100644 Topers.Client/src/pages/MainPage.jsx create mode 100644 Topers.Client/src/shared/Cart.jsx create mode 100644 Topers.Client/src/widgets/Header.jsx diff --git a/Topers.Client/index.html b/Topers.Client/index.html index 4f698af..bcaafb3 100644 --- a/Topers.Client/index.html +++ b/Topers.Client/index.html @@ -2,8 +2,8 @@ - - + + Topers diff --git a/Topers.Client/package-lock.json b/Topers.Client/package-lock.json index c6873fb..f578417 100644 --- a/Topers.Client/package-lock.json +++ b/Topers.Client/package-lock.json @@ -8,8 +8,14 @@ "name": "topers-client", "version": "0.0.0", "dependencies": { + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "framer-motion": "^11.3.4", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-icons": "^5.2.1" }, "devDependencies": { "@types/react": "^18.3.3", @@ -40,7 +46,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/highlight": "^7.24.7", @@ -95,7 +100,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7", @@ -128,7 +132,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" @@ -141,7 +144,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.24.7", @@ -155,243 +157,1669 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chakra-ui/accordion": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/accordion/-/accordion-2.3.1.tgz", + "integrity": "sha512-FSXRm8iClFyU+gVaXisOSEw0/4Q+qZbFRiuhIAkVU6Boj0FxAMrlo9a8AV5TuF77rgaHytCdHk0Ng+cyUijrag==", + "license": "MIT", + "dependencies": { + "@chakra-ui/descendant": "3.1.0", + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/transition": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/alert": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/alert/-/alert-2.2.2.tgz", + "integrity": "sha512-jHg4LYMRNOJH830ViLuicjb3F+v6iriE/2G5T+Sd0Hna04nukNJ1MxUmBPE+vI22me2dIflfelu2v9wdB6Pojw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/spinner": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/anatomy": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/anatomy/-/anatomy-2.2.2.tgz", + "integrity": "sha512-MV6D4VLRIHr4PkW4zMyqfrNS1mPlCTiCXwvYGtDFQYr+xHFfonhAuf9WjsSc0nyp2m0OdkSLnzmVKkZFLo25Tg==", + "license": "MIT" + }, + "node_modules/@chakra-ui/avatar": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/avatar/-/avatar-2.3.0.tgz", + "integrity": "sha512-8gKSyLfygnaotbJbDMHDiJoF38OHXUYVme4gGxZ1fLnQEdPVEaIWfH+NndIjOM0z8S+YEFnT9KyGMUtvPrBk3g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/image": "2.1.0", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/breadcrumb": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/breadcrumb/-/breadcrumb-2.2.0.tgz", + "integrity": "sha512-4cWCG24flYBxjruRi4RJREWTGF74L/KzI2CognAW/d/zWR0CjiScuJhf37Am3LFbCySP6WSoyBOtTIoTA4yLEA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/breakpoint-utils": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@chakra-ui/breakpoint-utils/-/breakpoint-utils-2.0.8.tgz", + "integrity": "sha512-Pq32MlEX9fwb5j5xx8s18zJMARNHlQZH2VH1RZgfgRDpp7DcEgtRW5AInfN5CfqdHLO1dGxA7I3MqEuL5JnIsA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5" + } + }, + "node_modules/@chakra-ui/button": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/button/-/button-2.1.0.tgz", + "integrity": "sha512-95CplwlRKmmUXkdEp/21VkEWgnwcx2TOBG6NfYlsuLBDHSLlo5FKIiE2oSi4zXc4TLcopGcWPNcm/NDaSC5pvA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/spinner": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/card": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/card/-/card-2.2.0.tgz", + "integrity": "sha512-xUB/k5MURj4CtPAhdSoXZidUbm8j3hci9vnc+eZJVDqhDOShNlD6QeniQNRPRys4lWAQLCbFcrwL29C8naDi6g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/checkbox": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/checkbox/-/checkbox-2.3.2.tgz", + "integrity": "sha512-85g38JIXMEv6M+AcyIGLh7igNtfpAN6KGQFYxY9tBj0eWvWk4NKQxvqqyVta0bSAyIl1rixNIIezNpNWk2iO4g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-callback-ref": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/visually-hidden": "2.2.0", + "@zag-js/focus-visible": "0.16.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/clickable": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/clickable/-/clickable-2.1.0.tgz", + "integrity": "sha512-flRA/ClPUGPYabu+/GLREZVZr9j2uyyazCAUHAdrTUEdDYCr31SVGhgh7dgKdtq23bOvAQJpIJjw/0Bs0WvbXw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/close-button": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/close-button/-/close-button-2.1.1.tgz", + "integrity": "sha512-gnpENKOanKexswSVpVz7ojZEALl2x5qjLYNqSQGbxz+aP9sOXPfUS56ebyBrre7T7exuWGiFeRwnM0oVeGPaiw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/color-mode": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/color-mode/-/color-mode-2.2.0.tgz", + "integrity": "sha512-niTEA8PALtMWRI9wJ4LL0CSBDo8NBfLNp4GD6/0hstcm3IlbBHTVKxN6HwSaoNYfphDQLxCjT4yG+0BJA5tFpg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-safe-layout-effect": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/control-box": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/control-box/-/control-box-2.1.0.tgz", + "integrity": "sha512-gVrRDyXFdMd8E7rulL0SKeoljkLQiPITFnsyMO8EFHNZ+AHt5wK4LIguYVEq88APqAGZGfHFWXr79RYrNiE3Mg==", + "license": "MIT", + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/counter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/counter/-/counter-2.1.0.tgz", + "integrity": "sha512-s6hZAEcWT5zzjNz2JIWUBzRubo9la/oof1W7EKZVVfPYHERnl5e16FmBC79Yfq8p09LQ+aqFKm/etYoJMMgghw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/number-utils": "2.0.7", + "@chakra-ui/react-use-callback-ref": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/css-reset": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-2.3.0.tgz", + "integrity": "sha512-cQwwBy5O0jzvl0K7PLTLgp8ijqLPKyuEMiDXwYzl95seD3AoeuoCLyzZcJtVqaUZ573PiBdAbY/IlZcwDOItWg==", + "license": "MIT", + "peerDependencies": { + "@emotion/react": ">=10.0.35", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/descendant": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/descendant/-/descendant-3.1.0.tgz", + "integrity": "sha512-VxCIAir08g5w27klLyi7PVo8BxhW4tgU/lxQyujkmi4zx7hT9ZdrcQLAted/dAa+aSIZ14S1oV0Q9lGjsAdxUQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/dom-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/dom-utils/-/dom-utils-2.1.0.tgz", + "integrity": "sha512-ZmF2qRa1QZ0CMLU8M1zCfmw29DmPNtfjR9iTo74U5FPr3i1aoAh7fbJ4qAlZ197Xw9eAW28tvzQuoVWeL5C7fQ==", + "license": "MIT" + }, + "node_modules/@chakra-ui/editable": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/editable/-/editable-3.1.0.tgz", + "integrity": "sha512-j2JLrUL9wgg4YA6jLlbU88370eCRyor7DZQD9lzpY95tSOXpTljeg3uF9eOmDnCs6fxp3zDWIfkgMm/ExhcGTg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-callback-ref": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/event-utils": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@chakra-ui/event-utils/-/event-utils-2.0.8.tgz", + "integrity": "sha512-IGM/yGUHS+8TOQrZGpAKOJl/xGBrmRYJrmbHfUE7zrG3PpQyXvbLDP1M+RggkCFVgHlJi2wpYIf0QtQlU0XZfw==", + "license": "MIT" + }, + "node_modules/@chakra-ui/focus-lock": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/focus-lock/-/focus-lock-2.1.0.tgz", + "integrity": "sha512-EmGx4PhWGjm4dpjRqM4Aa+rCWBxP+Rq8Uc/nAVnD4YVqkEhBkrPTpui2lnjsuxqNaZ24fIAZ10cF1hlpemte/w==", + "license": "MIT", + "dependencies": { + "@chakra-ui/dom-utils": "2.1.0", + "react-focus-lock": "^2.9.4" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/form-control": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/form-control/-/form-control-2.2.0.tgz", + "integrity": "sha512-wehLC1t4fafCVJ2RvJQT2jyqsAwX7KymmiGqBu7nQoQz8ApTkGABWpo/QwDh3F/dBLrouHDoOvGmYTqft3Mirw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/hooks": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/hooks/-/hooks-2.2.1.tgz", + "integrity": "sha512-RQbTnzl6b1tBjbDPf9zGRo9rf/pQMholsOudTxjy4i9GfTfz6kgp5ValGjQm2z7ng6Z31N1cnjZ1AlSzQ//ZfQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-utils": "2.0.12", + "@chakra-ui/utils": "2.0.15", + "compute-scroll-into-view": "3.0.3", + "copy-to-clipboard": "3.3.3" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/icon": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/icon/-/icon-3.2.0.tgz", + "integrity": "sha512-xxjGLvlX2Ys4H0iHrI16t74rG9EBcpFvJ3Y3B7KMQTrnW34Kf7Da/UC8J67Gtx85mTHW020ml85SVPKORWNNKQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/icons": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/icons/-/icons-2.1.1.tgz", + "integrity": "sha512-3p30hdo4LlRZTT5CwoAJq3G9fHI0wDc0pBaMHj4SUn0yomO+RcDRlzhdXqdr5cVnzax44sqXJVnf3oQG0eI+4g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/image": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/image/-/image-2.1.0.tgz", + "integrity": "sha512-bskumBYKLiLMySIWDGcz0+D9Th0jPvmX6xnRMs4o92tT3Od/bW26lahmV2a2Op2ItXeCmRMY+XxJH5Gy1i46VA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/input": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/input/-/input-2.1.2.tgz", + "integrity": "sha512-GiBbb3EqAA8Ph43yGa6Mc+kUPjh4Spmxp1Pkelr8qtudpc3p2PJOOebLpd90mcqw8UePPa+l6YhhPtp6o0irhw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/object-utils": "2.1.0", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/layout": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/layout/-/layout-2.3.1.tgz", + "integrity": "sha512-nXuZ6WRbq0WdgnRgLw+QuxWAHuhDtVX8ElWqcTK+cSMFg/52eVP47czYBE5F35YhnoW2XBwfNoNgZ7+e8Z01Rg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/breakpoint-utils": "2.0.8", + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/object-utils": "2.1.0", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/lazy-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@chakra-ui/lazy-utils/-/lazy-utils-2.0.5.tgz", + "integrity": "sha512-UULqw7FBvcckQk2n3iPO56TMJvDsNv0FKZI6PlUNJVaGsPbsYxK/8IQ60vZgaTVPtVcjY6BE+y6zg8u9HOqpyg==", + "license": "MIT" + }, + "node_modules/@chakra-ui/live-region": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/live-region/-/live-region-2.1.0.tgz", + "integrity": "sha512-ZOxFXwtaLIsXjqnszYYrVuswBhnIHHP+XIgK1vC6DePKtyK590Wg+0J0slDwThUAd4MSSIUa/nNX84x1GMphWw==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/media-query": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/media-query/-/media-query-3.3.0.tgz", + "integrity": "sha512-IsTGgFLoICVoPRp9ykOgqmdMotJG0CnPsKvGQeSFOB/dZfIujdVb14TYxDU4+MURXry1MhJ7LzZhv+Ml7cr8/g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/breakpoint-utils": "2.0.8", + "@chakra-ui/react-env": "3.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/menu": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/menu/-/menu-2.2.1.tgz", + "integrity": "sha512-lJS7XEObzJxsOwWQh7yfG4H8FzFPRP5hVPN/CL+JzytEINCSBvsCDHrYPQGp7jzpCi8vnTqQQGQe0f8dwnXd2g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/clickable": "2.1.0", + "@chakra-ui/descendant": "3.1.0", + "@chakra-ui/lazy-utils": "2.0.5", + "@chakra-ui/popper": "3.1.0", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-animation-state": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-disclosure": "2.1.0", + "@chakra-ui/react-use-focus-effect": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-outside-click": "2.2.0", + "@chakra-ui/react-use-update-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/transition": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/modal": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/modal/-/modal-2.3.1.tgz", + "integrity": "sha512-TQv1ZaiJMZN+rR9DK0snx/OPwmtaGH1HbZtlYt4W4s6CzyK541fxLRTjIXfEzIGpvNW+b6VFuFjbcR78p4DEoQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/close-button": "2.1.1", + "@chakra-ui/focus-lock": "2.1.0", + "@chakra-ui/portal": "2.1.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/transition": "2.1.0", + "aria-hidden": "^1.2.3", + "react-remove-scroll": "^2.5.6" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@chakra-ui/number-input": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/number-input/-/number-input-2.1.2.tgz", + "integrity": "sha512-pfOdX02sqUN0qC2ysuvgVDiws7xZ20XDIlcNhva55Jgm095xjm8eVdIBfNm3SFbSUNxyXvLTW/YQanX74tKmuA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/counter": "2.1.0", + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-callback-ref": "2.1.0", + "@chakra-ui/react-use-event-listener": "2.1.0", + "@chakra-ui/react-use-interval": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/number-utils": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@chakra-ui/number-utils/-/number-utils-2.0.7.tgz", + "integrity": "sha512-yOGxBjXNvLTBvQyhMDqGU0Oj26s91mbAlqKHiuw737AXHt0aPllOthVUqQMeaYLwLCjGMg0jtI7JReRzyi94Dg==", + "license": "MIT" + }, + "node_modules/@chakra-ui/object-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/object-utils/-/object-utils-2.1.0.tgz", + "integrity": "sha512-tgIZOgLHaoti5PYGPTwK3t/cqtcycW0owaiOXoZOcpwwX/vlVb+H1jFsQyWiiwQVPt9RkoSLtxzXamx+aHH+bQ==", + "license": "MIT" + }, + "node_modules/@chakra-ui/pin-input": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/pin-input/-/pin-input-2.1.0.tgz", + "integrity": "sha512-x4vBqLStDxJFMt+jdAHHS8jbh294O53CPQJoL4g228P513rHylV/uPscYUHrVJXRxsHfRztQO9k45jjTYaPRMw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/descendant": "3.1.0", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/popover": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/popover/-/popover-2.2.1.tgz", + "integrity": "sha512-K+2ai2dD0ljvJnlrzesCDT9mNzLifE3noGKZ3QwLqd/K34Ym1W/0aL1ERSynrcG78NKoXS54SdEzkhCZ4Gn/Zg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/close-button": "2.1.1", + "@chakra-ui/lazy-utils": "2.0.5", + "@chakra-ui/popper": "3.1.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-animation-state": "2.1.0", + "@chakra-ui/react-use-disclosure": "2.1.0", + "@chakra-ui/react-use-focus-effect": "2.1.0", + "@chakra-ui/react-use-focus-on-pointer-down": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/popper": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/popper/-/popper-3.1.0.tgz", + "integrity": "sha512-ciDdpdYbeFG7og6/6J8lkTFxsSvwTdMLFkpVylAF6VNC22jssiWfquj2eyD4rJnzkRFPvIWJq8hvbfhsm+AjSg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@popperjs/core": "^2.9.3" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/portal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/portal/-/portal-2.1.0.tgz", + "integrity": "sha512-9q9KWf6SArEcIq1gGofNcFPSWEyl+MfJjEUg/un1SMlQjaROOh3zYr+6JAwvcORiX7tyHosnmWC3d3wI2aPSQg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@chakra-ui/progress": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/progress/-/progress-2.2.0.tgz", + "integrity": "sha512-qUXuKbuhN60EzDD9mHR7B67D7p/ZqNS2Aze4Pbl1qGGZfulPW0PY8Rof32qDtttDQBkzQIzFGE8d9QpAemToIQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/provider": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/provider/-/provider-2.4.2.tgz", + "integrity": "sha512-w0Tef5ZCJK1mlJorcSjItCSbyvVuqpvyWdxZiVQmE6fvSJR83wZof42ux0+sfWD+I7rHSfj+f9nzhNaEWClysw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/css-reset": "2.3.0", + "@chakra-ui/portal": "2.1.0", + "@chakra-ui/react-env": "3.1.0", + "@chakra-ui/system": "2.6.2", + "@chakra-ui/utils": "2.0.15" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@chakra-ui/radio": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/radio/-/radio-2.1.2.tgz", + "integrity": "sha512-n10M46wJrMGbonaghvSRnZ9ToTv/q76Szz284gv4QUWvyljQACcGrXIONUnQ3BIwbOfkRqSk7Xl/JgZtVfll+w==", + "license": "MIT", + "dependencies": { + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@zag-js/focus-visible": "0.16.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/react/-/react-2.8.2.tgz", + "integrity": "sha512-Hn0moyxxyCDKuR9ywYpqgX8dvjqwu9ArwpIb9wHNYjnODETjLwazgNIliCVBRcJvysGRiV51U2/JtJVrpeCjUQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/accordion": "2.3.1", + "@chakra-ui/alert": "2.2.2", + "@chakra-ui/avatar": "2.3.0", + "@chakra-ui/breadcrumb": "2.2.0", + "@chakra-ui/button": "2.1.0", + "@chakra-ui/card": "2.2.0", + "@chakra-ui/checkbox": "2.3.2", + "@chakra-ui/close-button": "2.1.1", + "@chakra-ui/control-box": "2.1.0", + "@chakra-ui/counter": "2.1.0", + "@chakra-ui/css-reset": "2.3.0", + "@chakra-ui/editable": "3.1.0", + "@chakra-ui/focus-lock": "2.1.0", + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/hooks": "2.2.1", + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/image": "2.1.0", + "@chakra-ui/input": "2.1.2", + "@chakra-ui/layout": "2.3.1", + "@chakra-ui/live-region": "2.1.0", + "@chakra-ui/media-query": "3.3.0", + "@chakra-ui/menu": "2.2.1", + "@chakra-ui/modal": "2.3.1", + "@chakra-ui/number-input": "2.1.2", + "@chakra-ui/pin-input": "2.1.0", + "@chakra-ui/popover": "2.2.1", + "@chakra-ui/popper": "3.1.0", + "@chakra-ui/portal": "2.1.0", + "@chakra-ui/progress": "2.2.0", + "@chakra-ui/provider": "2.4.2", + "@chakra-ui/radio": "2.1.2", + "@chakra-ui/react-env": "3.1.0", + "@chakra-ui/select": "2.1.2", + "@chakra-ui/skeleton": "2.1.0", + "@chakra-ui/skip-nav": "2.1.0", + "@chakra-ui/slider": "2.1.0", + "@chakra-ui/spinner": "2.1.0", + "@chakra-ui/stat": "2.1.1", + "@chakra-ui/stepper": "2.3.1", + "@chakra-ui/styled-system": "2.9.2", + "@chakra-ui/switch": "2.1.2", + "@chakra-ui/system": "2.6.2", + "@chakra-ui/table": "2.1.0", + "@chakra-ui/tabs": "3.0.0", + "@chakra-ui/tag": "3.1.1", + "@chakra-ui/textarea": "2.1.2", + "@chakra-ui/theme": "3.3.1", + "@chakra-ui/theme-utils": "2.0.21", + "@chakra-ui/toast": "7.0.2", + "@chakra-ui/tooltip": "2.3.1", + "@chakra-ui/transition": "2.1.0", + "@chakra-ui/utils": "2.0.15", + "@chakra-ui/visually-hidden": "2.2.0" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@chakra-ui/react-children-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-children-utils/-/react-children-utils-2.0.6.tgz", + "integrity": "sha512-QVR2RC7QsOsbWwEnq9YduhpqSFnZGvjjGREV8ygKi8ADhXh93C8azLECCUVgRJF2Wc+So1fgxmjLcbZfY2VmBA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-context": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-context/-/react-context-2.1.0.tgz", + "integrity": "sha512-iahyStvzQ4AOwKwdPReLGfDesGG+vWJfEsn0X/NoGph/SkN+HXtv2sCfYFFR9k7bb+Kvc6YfpLlSuLvKMHi2+w==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-env": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-env/-/react-env-3.1.0.tgz", + "integrity": "sha512-Vr96GV2LNBth3+IKzr/rq1IcnkXv+MLmwjQH6C8BRtn3sNskgDFD5vLkVXcEhagzZMCh8FR3V/bzZPojBOyNhw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-safe-layout-effect": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-types/-/react-types-2.0.7.tgz", + "integrity": "sha512-12zv2qIZ8EHwiytggtGvo4iLT0APris7T0qaAWqzpUGS0cdUtR8W+V1BJ5Ocq+7tA6dzQ/7+w5hmXih61TuhWQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-animation-state": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-animation-state/-/react-use-animation-state-2.1.0.tgz", + "integrity": "sha512-CFZkQU3gmDBwhqy0vC1ryf90BVHxVN8cTLpSyCpdmExUEtSEInSCGMydj2fvn7QXsz/za8JNdO2xxgJwxpLMtg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/dom-utils": "2.1.0", + "@chakra-ui/react-use-event-listener": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-callback-ref": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-callback-ref/-/react-use-callback-ref-2.1.0.tgz", + "integrity": "sha512-efnJrBtGDa4YaxDzDE90EnKD3Vkh5a1t3w7PhnRQmsphLy3g2UieasoKTlT2Hn118TwDjIv5ZjHJW6HbzXA9wQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-controllable-state": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-controllable-state/-/react-use-controllable-state-2.1.0.tgz", + "integrity": "sha512-QR/8fKNokxZUs4PfxjXuwl0fj/d71WPrmLJvEpCTkHjnzu7LnYvzoe2wB867IdooQJL0G1zBxl0Dq+6W1P3jpg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-disclosure": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-disclosure/-/react-use-disclosure-2.1.0.tgz", + "integrity": "sha512-Ax4pmxA9LBGMyEZJhhUZobg9C0t3qFE4jVF1tGBsrLDcdBeLR9fwOogIPY9Hf0/wqSlAryAimICbr5hkpa5GSw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-event-listener": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-event-listener/-/react-use-event-listener-2.1.0.tgz", + "integrity": "sha512-U5greryDLS8ISP69DKDsYcsXRtAdnTQT+jjIlRYZ49K/XhUR/AqVZCK5BkR1spTDmO9H8SPhgeNKI70ODuDU/Q==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-focus-effect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-effect/-/react-use-focus-effect-2.1.0.tgz", + "integrity": "sha512-xzVboNy7J64xveLcxTIJ3jv+lUJKDwRM7Szwn9tNzUIPD94O3qwjV7DDCUzN2490nSYDF4OBMt/wuDBtaR3kUQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/dom-utils": "2.1.0", + "@chakra-ui/react-use-event-listener": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-focus-on-pointer-down": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-focus-on-pointer-down/-/react-use-focus-on-pointer-down-2.1.0.tgz", + "integrity": "sha512-2jzrUZ+aiCG/cfanrolsnSMDykCAbv9EK/4iUyZno6BYb3vziucmvgKuoXbMPAzWNtwUwtuMhkby8rc61Ue+Lg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-event-listener": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-interval": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-interval/-/react-use-interval-2.1.0.tgz", + "integrity": "sha512-8iWj+I/+A0J08pgEXP1J1flcvhLBHkk0ln7ZvGIyXiEyM6XagOTJpwNhiu+Bmk59t3HoV/VyvyJTa+44sEApuw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-latest-ref": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-latest-ref/-/react-use-latest-ref-2.1.0.tgz", + "integrity": "sha512-m0kxuIYqoYB0va9Z2aW4xP/5b7BzlDeWwyXCH6QpT2PpW3/281L3hLCm1G0eOUcdVlayqrQqOeD6Mglq+5/xoQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-merge-refs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-merge-refs/-/react-use-merge-refs-2.1.0.tgz", + "integrity": "sha512-lERa6AWF1cjEtWSGjxWTaSMvneccnAVH4V4ozh8SYiN9fSPZLlSG3kNxfNzdFvMEhM7dnP60vynF7WjGdTgQbQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-outside-click": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-outside-click/-/react-use-outside-click-2.2.0.tgz", + "integrity": "sha512-PNX+s/JEaMneijbgAM4iFL+f3m1ga9+6QK0E5Yh4s8KZJQ/bLwZzdhMz8J/+mL+XEXQ5J0N8ivZN28B82N1kNw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-pan-event": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-pan-event/-/react-use-pan-event-2.1.0.tgz", + "integrity": "sha512-xmL2qOHiXqfcj0q7ZK5s9UjTh4Gz0/gL9jcWPA6GVf+A0Od5imEDa/Vz+533yQKWiNSm1QGrIj0eJAokc7O4fg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/event-utils": "2.0.8", + "@chakra-ui/react-use-latest-ref": "2.1.0", + "framesync": "6.1.2" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-previous": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-previous/-/react-use-previous-2.1.0.tgz", + "integrity": "sha512-pjxGwue1hX8AFcmjZ2XfrQtIJgqbTF3Qs1Dy3d1krC77dEsiCUbQ9GzOBfDc8pfd60DrB5N2tg5JyHbypqh0Sg==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-safe-layout-effect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-safe-layout-effect/-/react-use-safe-layout-effect-2.1.0.tgz", + "integrity": "sha512-Knbrrx/bcPwVS1TorFdzrK/zWA8yuU/eaXDkNj24IrKoRlQrSBFarcgAEzlCHtzuhufP3OULPkELTzz91b0tCw==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-size": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-size/-/react-use-size-2.1.0.tgz", + "integrity": "sha512-tbLqrQhbnqOjzTaMlYytp7wY8BW1JpL78iG7Ru1DlV4EWGiAmXFGvtnEt9HftU0NJ0aJyjgymkxfVGI55/1Z4A==", + "license": "MIT", + "dependencies": { + "@zag-js/element-size": "0.10.5" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-timeout": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-timeout/-/react-use-timeout-2.1.0.tgz", + "integrity": "sha512-cFN0sobKMM9hXUhyCofx3/Mjlzah6ADaEl/AXl5Y+GawB5rgedgAcu2ErAgarEkwvsKdP6c68CKjQ9dmTQlJxQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-use-callback-ref": "2.1.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-use-update-effect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-use-update-effect/-/react-use-update-effect-2.1.0.tgz", + "integrity": "sha512-ND4Q23tETaR2Qd3zwCKYOOS1dfssojPLJMLvUtUbW5M9uW1ejYWgGUobeAiOVfSplownG8QYMmHTP86p/v0lbA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/react-utils": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@chakra-ui/react-utils/-/react-utils-2.0.12.tgz", + "integrity": "sha512-GbSfVb283+YA3kA8w8xWmzbjNWk14uhNpntnipHCftBibl0lxtQ9YqMFQLwuFOO0U2gYVocszqqDWX+XNKq9hw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/utils": "2.0.15" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@chakra-ui/select": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/select/-/select-2.1.2.tgz", + "integrity": "sha512-ZwCb7LqKCVLJhru3DXvKXpZ7Pbu1TDZ7N0PdQ0Zj1oyVLJyrpef1u9HR5u0amOpqcH++Ugt0f5JSmirjNlctjA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/shared-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@chakra-ui/shared-utils/-/shared-utils-2.0.5.tgz", + "integrity": "sha512-4/Wur0FqDov7Y0nCXl7HbHzCg4aq86h+SXdoUeuCMD3dSj7dpsVnStLYhng1vxvlbUnLpdF4oz5Myt3i/a7N3Q==", + "license": "MIT" + }, + "node_modules/@chakra-ui/skeleton": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/skeleton/-/skeleton-2.1.0.tgz", + "integrity": "sha512-JNRuMPpdZGd6zFVKjVQ0iusu3tXAdI29n4ZENYwAJEMf/fN0l12sVeirOxkJ7oEL0yOx2AgEYFSKdbcAgfUsAQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/media-query": "3.3.0", + "@chakra-ui/react-use-previous": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/skip-nav": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/skip-nav/-/skip-nav-2.1.0.tgz", + "integrity": "sha512-Hk+FG+vadBSH0/7hwp9LJnLjkO0RPGnx7gBJWI4/SpoJf3e4tZlWYtwGj0toYY4aGKl93jVghuwGbDBEMoHDug==", + "license": "MIT", + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/slider": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/slider/-/slider-2.1.0.tgz", + "integrity": "sha512-lUOBcLMCnFZiA/s2NONXhELJh6sY5WtbRykPtclGfynqqOo47lwWJx+VP7xaeuhDOPcWSSecWc9Y1BfPOCz9cQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/number-utils": "2.0.7", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-callback-ref": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-latest-ref": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-pan-event": "2.1.0", + "@chakra-ui/react-use-size": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/spinner": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/spinner/-/spinner-2.1.0.tgz", + "integrity": "sha512-hczbnoXt+MMv/d3gE+hjQhmkzLiKuoTo42YhUG7Bs9OSv2lg1fZHW1fGNRFP3wTi6OIbD044U1P9HK+AOgFH3g==", + "license": "MIT", + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/stat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/stat/-/stat-2.1.1.tgz", + "integrity": "sha512-LDn0d/LXQNbAn2KaR3F1zivsZCewY4Jsy1qShmfBMKwn6rI8yVlbvu6SiA3OpHS0FhxbsZxQI6HefEoIgtqY6Q==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/stepper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/stepper/-/stepper-2.3.1.tgz", + "integrity": "sha512-ky77lZbW60zYkSXhYz7kbItUpAQfEdycT0Q4bkHLxfqbuiGMf8OmgZOQkOB9uM4v0zPwy2HXhe0vq4Dd0xa55Q==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/styled-system": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/styled-system/-/styled-system-2.9.2.tgz", + "integrity": "sha512-To/Z92oHpIE+4nk11uVMWqo2GGRS86coeMmjxtpnErmWRdLcp1WVCVRAvn+ZwpLiNR+reWFr2FFqJRsREuZdAg==", + "license": "MIT", + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5", + "csstype": "^3.1.2", + "lodash.mergewith": "4.6.2" + } + }, + "node_modules/@chakra-ui/switch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/switch/-/switch-2.1.2.tgz", + "integrity": "sha512-pgmi/CC+E1v31FcnQhsSGjJnOE2OcND4cKPyTE+0F+bmGm48Q/b5UmKD9Y+CmZsrt/7V3h8KNczowupfuBfIHA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/checkbox": "2.3.2", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/system": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/system/-/system-2.6.2.tgz", + "integrity": "sha512-EGtpoEjLrUu4W1fHD+a62XR+hzC5YfsWm+6lO0Kybcga3yYEij9beegO0jZgug27V+Rf7vns95VPVP6mFd/DEQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/color-mode": "2.2.0", + "@chakra-ui/object-utils": "2.1.0", + "@chakra-ui/react-utils": "2.0.12", + "@chakra-ui/styled-system": "2.9.2", + "@chakra-ui/theme-utils": "2.0.21", + "@chakra-ui/utils": "2.0.15", + "react-fast-compare": "3.2.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0", + "@emotion/styled": "^11.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/table/-/table-2.1.0.tgz", + "integrity": "sha512-o5OrjoHCh5uCLdiUb0Oc0vq9rIAeHSIRScc2ExTC9Qg/uVZl2ygLrjToCaKfaaKl1oQexIeAcZDKvPG8tVkHyQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/tabs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/tabs/-/tabs-3.0.0.tgz", + "integrity": "sha512-6Mlclp8L9lqXmsGWF5q5gmemZXOiOYuh0SGT/7PgJVNPz3LXREXlXg2an4MBUD8W5oTkduCX+3KTMCwRrVrDYw==", + "license": "MIT", + "dependencies": { + "@chakra-ui/clickable": "2.1.0", + "@chakra-ui/descendant": "3.1.0", + "@chakra-ui/lazy-utils": "2.0.5", + "@chakra-ui/react-children-utils": "2.0.6", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-controllable-state": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/react-use-safe-layout-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/tag": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tag/-/tag-3.1.1.tgz", + "integrity": "sha512-Bdel79Dv86Hnge2PKOU+t8H28nm/7Y3cKd4Kfk9k3lOpUh4+nkSGe58dhRzht59lEqa4N9waCgQiBdkydjvBXQ==", + "license": "MIT", + "dependencies": { + "@chakra-ui/icon": "3.2.0", + "@chakra-ui/react-context": "2.1.0" + }, + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" + } + }, + "node_modules/@chakra-ui/textarea": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/textarea/-/textarea-2.1.2.tgz", + "integrity": "sha512-ip7tvklVCZUb2fOHDb23qPy/Fr2mzDOGdkrpbNi50hDCiV4hFX02jdQJdi3ydHZUyVgZVBKPOJ+lT9i7sKA2wA==", + "license": "MIT", + "dependencies": { + "@chakra-ui/form-control": "2.2.0", + "@chakra-ui/shared-utils": "2.0.5" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dev": true, + "node_modules/@chakra-ui/theme": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme/-/theme-3.3.1.tgz", + "integrity": "sha512-Hft/VaT8GYnItGCBbgWd75ICrIrIFrR7lVOhV/dQnqtfGqsVDlrztbSErvMkoPKt0UgAkd9/o44jmZ6X4U2nZQ==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@chakra-ui/anatomy": "2.2.2", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/theme-tools": "2.1.2" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@chakra-ui/styled-system": ">=2.8.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", - "dev": true, + "node_modules/@chakra-ui/theme-tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme-tools/-/theme-tools-2.1.2.tgz", + "integrity": "sha512-Qdj8ajF9kxY4gLrq7gA+Azp8CtFHGO9tWMN2wfF9aQNgG9AuMhPrUzMq9AMQ0MXiYcgNq/FD3eegB43nHVmXVA==", "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "@chakra-ui/anatomy": "2.2.2", + "@chakra-ui/shared-utils": "2.0.5", + "color2k": "^2.0.2" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@chakra-ui/styled-system": ">=2.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", - "dev": true, + "node_modules/@chakra-ui/theme-utils": { + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/@chakra-ui/theme-utils/-/theme-utils-2.0.21.tgz", + "integrity": "sha512-FjH5LJbT794r0+VSCXB3lT4aubI24bLLRWB+CuRKHijRvsOg717bRdUN/N1fEmEpFnRVrbewttWh/OQs0EWpWw==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/styled-system": "2.9.2", + "@chakra-ui/theme": "3.3.1", + "lodash.mergewith": "4.6.2" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, + "node_modules/@chakra-ui/toast": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@chakra-ui/toast/-/toast-7.0.2.tgz", + "integrity": "sha512-yvRP8jFKRs/YnkuE41BVTq9nB2v/KDRmje9u6dgDmE5+1bFt3bwjdf9gVbif4u5Ve7F7BGk5E093ARRVtvLvXA==", "license": "MIT", "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@chakra-ui/alert": "2.2.2", + "@chakra-ui/close-button": "2.1.1", + "@chakra-ui/portal": "2.1.0", + "@chakra-ui/react-context": "2.1.0", + "@chakra-ui/react-use-timeout": "2.1.0", + "@chakra-ui/react-use-update-effect": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5", + "@chakra-ui/styled-system": "2.9.2", + "@chakra-ui/theme": "3.3.1" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@chakra-ui/system": "2.6.2", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", - "dev": true, + "node_modules/@chakra-ui/tooltip": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@chakra-ui/tooltip/-/tooltip-2.3.1.tgz", + "integrity": "sha512-Rh39GBn/bL4kZpuEMPPRwYNnccRCL+w9OqamWHIB3Qboxs6h8cOyXfIdGxjo72lvhu1QI/a4KFqkM3St+WfC0A==", "license": "MIT", "dependencies": { - "@babel/types": "^7.24.7" + "@chakra-ui/dom-utils": "2.1.0", + "@chakra-ui/popper": "3.1.0", + "@chakra-ui/portal": "2.1.0", + "@chakra-ui/react-types": "2.0.7", + "@chakra-ui/react-use-disclosure": "2.1.0", + "@chakra-ui/react-use-event-listener": "2.1.0", + "@chakra-ui/react-use-merge-refs": "2.1.0", + "@chakra-ui/shared-utils": "2.0.5" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "framer-motion": ">=4.0.0", + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", - "dev": true, + "node_modules/@chakra-ui/transition": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/transition/-/transition-2.1.0.tgz", + "integrity": "sha512-orkT6T/Dt+/+kVwJNy7zwJ+U2xAZ3EU7M3XCs45RBvUnZDr/u9vdmaM/3D/rOpmQJWgQBwKPJleUXrYWUagEDQ==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@chakra-ui/shared-utils": "2.0.5" + }, + "peerDependencies": { + "framer-motion": ">=4.0.0", + "react": ">=18" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, + "node_modules/@chakra-ui/utils": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@chakra-ui/utils/-/utils-2.0.15.tgz", + "integrity": "sha512-El4+jL0WSaYYs+rJbuYFDbjmfCcfGDmRY95GO4xwzit6YAPZBLcR65rOEwLps+XWluZTy1xdMrusg/hW0c1aAA==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "dependencies": { + "@types/lodash.mergewith": "4.6.7", + "css-box-model": "1.2.1", + "framesync": "6.1.2", + "lodash.mergewith": "4.6.2" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", - "dev": true, + "node_modules/@chakra-ui/visually-hidden": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@chakra-ui/visually-hidden/-/visually-hidden-2.2.0.tgz", + "integrity": "sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ==", "license": "MIT", - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@chakra-ui/system": ">=2.0.0", + "react": ">=18" } }, - "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dev": true, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dev": true, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, "engines": { - "node": ">=6.9.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", - "dev": true, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@emotion/memoize": "^0.8.1" } }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", - "dev": true, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", - "dev": true, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" } }, - "node_modules/@babel/traverse": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", - "dev": true, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.11.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz", + "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.2", + "@emotion/serialize": "^1.1.4", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", - "dev": true, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "react": ">=16.8.0" } }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -901,7 +2329,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -916,7 +2343,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -926,7 +2352,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -936,14 +2361,12 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -988,6 +2411,16 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -1264,18 +2697,39 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "license": "MIT" + }, + "node_modules/@types/lodash.mergewith": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/@types/lodash.mergewith/-/lodash.mergewith-4.6.7.tgz", + "integrity": "sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1319,6 +2773,27 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@zag-js/dom-query": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@zag-js/dom-query/-/dom-query-0.16.0.tgz", + "integrity": "sha512-Oqhd6+biWyKnhKwFFuZrrf6lxBz2tX2pRQe6grUnYwO6HJ8BcbqZomy2lpOdr+3itlaUqx+Ywj5E5ZZDr/LBfQ==", + "license": "MIT" + }, + "node_modules/@zag-js/element-size": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@zag-js/element-size/-/element-size-0.10.5.tgz", + "integrity": "sha512-uQre5IidULANvVkNOBQ1tfgwTQcGl4hliPSe69Fct1VfYb2Fd0jdAcGzqQgPhfrXFpR62MxLPB7erxJ/ngtL8w==", + "license": "MIT" + }, + "node_modules/@zag-js/focus-visible": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@zag-js/focus-visible/-/focus-visible-0.16.0.tgz", + "integrity": "sha512-a7U/HSopvQbrDU4GLerpqiMcHKEkQkNPeDZJWz38cw/6Upunh41GjHetq5TB84hxyCaDzJ6q2nEdNoBQfC0FKA==", + "license": "MIT", + "dependencies": { + "@zag-js/dom-query": "0.16.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -1373,7 +2848,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -1389,6 +2863,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -1555,6 +3041,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1630,7 +3148,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1661,7 +3178,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -1676,7 +3192,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -1686,7 +3201,18 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, + "license": "MIT" + }, + "node_modules/color2k": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz", + "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", + "license": "MIT" + }, + "node_modules/compute-scroll-into-view": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", + "integrity": "sha512-nadqwNxghAGTamwIqQSG433W6OADZx2vCo3UXHNrzTRHK/htu+7+L0zhjEoaeaQVNAi3YgqWDv8+tzf0hRfR+A==", "license": "MIT" }, "node_modules/concat-map": { @@ -1703,6 +3229,31 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1718,11 +3269,19 @@ "node": ">= 8" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -1783,7 +3342,6 @@ "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -1840,6 +3398,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1860,6 +3424,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -2079,7 +3652,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.0" @@ -2453,6 +4025,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2492,6 +4070,18 @@ "dev": true, "license": "ISC" }, + "node_modules/focus-lock": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.5.tgz", + "integrity": "sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -2502,6 +4092,46 @@ "is-callable": "^1.1.3" } }, + "node_modules/framer-motion": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.4.tgz", + "integrity": "sha512-LC+luwNmz4zEg0AU0rol3yLUFKSJ9GDmIyvigXBAwEbUBG59EWmcRQ2Xh+0py7IkmvWxFUH0TN42v1Dda8xgUg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/framesync": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz", + "integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==", + "license": "MIT", + "dependencies": { + "tslib": "2.4.0" + } + }, + "node_modules/framesync/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "license": "0BSD" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2528,7 +4158,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2593,6 +4222,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -2650,7 +4288,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2707,7 +4344,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2772,7 +4408,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2781,6 +4416,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2795,7 +4439,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2852,6 +4495,15 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -2869,6 +4521,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2932,7 +4590,6 @@ "version": "2.14.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -3268,7 +4925,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -3284,6 +4940,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3351,6 +5013,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3374,6 +5042,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3413,7 +5087,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3453,7 +5126,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -3635,7 +5307,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3644,6 +5315,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3678,14 +5367,21 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true, "license": "ISC" }, "node_modules/possible-typed-array-names": { @@ -3741,7 +5437,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -3792,6 +5487,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-clientside-effect": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", + "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -3805,11 +5512,48 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-focus-lock": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.12.1.tgz", + "integrity": "sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^1.3.5", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.6", + "use-callback-ref": "^1.3.2", + "use-sidecar": "^1.1.2" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-refresh": { @@ -3822,6 +5566,76 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.10.tgz", + "integrity": "sha512-m3zvBRANPBw3qxVVjEIPEQinkcwlFZ4qyomuWVpNJdv4c6MvHfXV0C3L9Jx5rr3HeBHKNRX+1jreB5QloDIJjA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -3844,6 +5658,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -3885,7 +5705,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4111,6 +5930,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4226,11 +6054,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -4243,7 +6076,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4259,16 +6091,33 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4429,6 +6278,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/vite": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", @@ -4608,6 +6500,15 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/Topers.Client/package.json b/Topers.Client/package.json index eb3fe7c..ab32b80 100644 --- a/Topers.Client/package.json +++ b/Topers.Client/package.json @@ -10,8 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "framer-motion": "^11.3.4", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-icons": "^5.2.1" }, "devDependencies": { "@types/react": "^18.3.3", diff --git a/Topers.Client/src/App.jsx b/Topers.Client/src/App.jsx deleted file mode 100644 index 5c4d3c1..0000000 --- a/Topers.Client/src/App.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useState } from 'react' - -function App() { - return ( - <> - ) -} - -export default App diff --git a/Topers.Client/src/app/images/logo.svg b/Topers.Client/src/app/images/logo.svg new file mode 100644 index 0000000..46a4041 --- /dev/null +++ b/Topers.Client/src/app/images/logo.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Topers.Client/src/app/images/site_logo.svg b/Topers.Client/src/app/images/site_logo.svg new file mode 100644 index 0000000..f8fe2a4 --- /dev/null +++ b/Topers.Client/src/app/images/site_logo.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Topers.Client/src/app/styles/Header.css b/Topers.Client/src/app/styles/Header.css new file mode 100644 index 0000000..936c2ce --- /dev/null +++ b/Topers.Client/src/app/styles/Header.css @@ -0,0 +1,11 @@ +.navlinks, .shop-cart-button { + cursor: pointer; +} + +.navbar-links { + gap: 30px; +} + +.active { + color: #dc3d3d; +} \ No newline at end of file diff --git a/Topers.Client/src/app/styles/index.css b/Topers.Client/src/app/styles/index.css new file mode 100644 index 0000000..647a824 --- /dev/null +++ b/Topers.Client/src/app/styles/index.css @@ -0,0 +1,13 @@ +body { + margin: 0; + padding: 0; +} + +.root { + background-color: #E1F9D9; +} + +.wrapper { + max-width: 1280px; + margin: auto; +} \ No newline at end of file diff --git a/Topers.Client/src/entities/Good.ts b/Topers.Client/src/entities/Good.ts new file mode 100644 index 0000000..cf8ef20 --- /dev/null +++ b/Topers.Client/src/entities/Good.ts @@ -0,0 +1,5 @@ +export interface Good { + id: string; + name: string; + price: number; +} \ No newline at end of file diff --git a/Topers.Client/src/index.css b/Topers.Client/src/index.css deleted file mode 100644 index e69de29..0000000 diff --git a/Topers.Client/src/main.jsx b/Topers.Client/src/main.jsx index 54b39dd..dd29994 100644 --- a/Topers.Client/src/main.jsx +++ b/Topers.Client/src/main.jsx @@ -1,7 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.jsx' -import './index.css' +import App from './pages/MainPage' +import './app/styles/index.css' ReactDOM.createRoot(document.getElementById('root')).render( diff --git a/Topers.Client/src/pages/MainPage.jsx b/Topers.Client/src/pages/MainPage.jsx new file mode 100644 index 0000000..0d71bac --- /dev/null +++ b/Topers.Client/src/pages/MainPage.jsx @@ -0,0 +1,16 @@ +import Header from '../widgets/Header' +import { ChakraProvider } from '@chakra-ui/react' + +function App() { + return ( + +
+
+
+
+
+
+ ) +} + +export default App diff --git a/Topers.Client/src/shared/Cart.jsx b/Topers.Client/src/shared/Cart.jsx new file mode 100644 index 0000000..f0a934e --- /dev/null +++ b/Topers.Client/src/shared/Cart.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { CiPillsBottle1 } from "react-icons/ci"; +import { Text, Box, IconButton, Icon } from '@chakra-ui/react'; + +export default function Cart({ cartItemCount, cartOpen, setCartOpen }) { + return ( + + } + variant="transparent" + size="lg" + fontSize="25px" + onClick={() => setCartOpen(cartOpen = !cartOpen)} + className={cartOpen ? 'active' : ''} + /> + {cartItemCount > 0 && ( + + + {cartItemCount} + + + )} + + ); +} diff --git a/Topers.Client/src/widgets/Header.jsx b/Topers.Client/src/widgets/Header.jsx new file mode 100644 index 0000000..ce1184c --- /dev/null +++ b/Topers.Client/src/widgets/Header.jsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react' +import { Image, Box, Divider, Button, Flex, Link, IconButton, Icon } from '@chakra-ui/react'; +import Logo from '../app/images/logo.svg'; +import '../app/styles/Header.css'; +import Cart from '../shared/Cart'; + +export default function Header() { + const [cartOpen, setCartOpen] = useState(false); + const [cartItemCount, setCartItemCount] = useState(2); + + return ( +
+ + + + + + + + Topers Logo + + + + About + + + Goods + + + Contacts + + + + + +
+ ) +} \ No newline at end of file From 97fadb1a912ea81bbe55c842bb3044082e02ffb5 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Wed, 17 Jul 2024 17:46:46 +0300 Subject: [PATCH 21/39] Design first-screen page --- .../src/app/images/stock_bg_hands.jpg | Bin 0 -> 20957 bytes Topers.Client/src/app/styles/index.css | 9 ---- Topers.Client/src/pages/MainPage.jsx | 26 +++++++--- Topers.Client/src/widgets/HeroSection.jsx | 47 ++++++++++++++++++ 4 files changed, 66 insertions(+), 16 deletions(-) create mode 100644 Topers.Client/src/app/images/stock_bg_hands.jpg create mode 100644 Topers.Client/src/widgets/HeroSection.jsx diff --git a/Topers.Client/src/app/images/stock_bg_hands.jpg b/Topers.Client/src/app/images/stock_bg_hands.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0a18b07fbea0f60436dd0625b9469a1356dee494 GIT binary patch literal 20957 zcmd?QbyS>57e3gyL*p9U-QC@t0D<6*1a}SY4#6c52pZf1f#B|zAR)NByKE;j-+YtV z$#>44J->f;-*cL-exG}vs#|sImhv?Fv?}w=g8D`$HfV;7Jasl6SkyQP&2h}6W`)Y`%cENI3|DrM(p%VmGb;-~P{hLl%nx!QH33;z*$I&!HMfzITA2xvYjY{FDmh4iEUjd{oIo00%9^HL z)~0-Bw? z`h(Nh#NO3Kh@AXSVh2lm7kg(*dxyX1f3fv1_@~AHQId0TG35jsAz|+X`b!U-hw0Bk zIlGv#{9WRw>Q7Ty$Vo~6%vT2F;^HZ1Yit2>mXVXz{EG$5%53oiv-*kstZ)k}bC+N8 zH#7Z9-oe$$=4Y;ErYs;E(2v%22FtR6v-+cU9c)~k|B?pV=IRXk@#?3JjhQ(MIKw}= z|5W&4hm;4nivFzbf3*uNz{1YT%FD#c&cx35S7u-`bt?-?mtS)Hzk80;PZ_Y*U)9Sm z;RG_X0%!WOUP%9_7#1rH7kg9dpF-rM|EMv3F=uCxi=3noIU5@vHybw(4+rZHoBu2T zzc$E3!v)+Ke$>bxwm5?w`G@aVIse7_vwCen7REL#Hddw}J7*A+@sAGmLrKKd#n#Xk zTq;*vA+WC*+gX`X8{0Yv{CMqb@9Ja<60)$jx3B^I_0|*&8(Ue}3H_)taJzu*1J^Xa zq_K;!l9e5}%l=iT|I!X__NK-rERsrcOseWCvU1{bno^QXQaTb+s+t;7V(JpIQpz%N z%2IO5l2STy$};4n;GSe=%5UlfGIp_d68_m=G(e7jBy6mNtt{-Ftel+%S$;S@`1c=) zpAsy8*8```{@X?VP#3dt`47(zr{D(%7jW4FEFxqEQF^(PlKlzba~}D+>Q`^xrwhPv`r~IV^1d z>ugN_?;S*th%n|M)9PgY3Ye z+5JZhemVdYtNthQk#aDz0(iycl%S!1jxy0ikg5QN9K{bUX?YDoSz^ax!WL9u{gkPC7DjRxvhC zJ^^82VJc=xISD~o9wA|YA0!ZnNJywCs6;>@kpL|@t-$}|=cyBb4iB*gX$A#>4uC|5 zfI^3O>IM*ihfFB&*#5KT{`dm{2?Y%U`vdzi=>K^B3;v1C1CXI00FXc^Ab7~VWBo7w z|CiLw6Cf(KP3qv%Aa?qH*z8leb)(&?pZzxb&QHPX(EqohP@9h;EFySE=SOIqUwq^C zU8Y=qD+^dV+uC?E#Ffe4UKG}Ctv?qregAuAxRA4^>qP^-%NakVvm1-%^IO+j+TU_R z=-iB@4-K!4eQdbp+iU((sr^0ox59wu!rog>r;VlsZ5m%H?U$;4F9|Pbk1LXWdUhow zh=zNy*E!!Y6s1mKYHKVZ~KPH3Tv#Lu8rCfryAtQUHrzl ztqo!O&8^h8ASk8swX~R*$G0T!W~MJJ9X*SNeMWT-UCx$fUoR&9YtD#$gZI%+YXrLo z)~LE{JKX)^4~Ad&X2#)0?z3+2wq~+l`+eIX{a4j9pURw@#yac5Oyh`A?hEg|=Gu{s z*GINXMy#7RXvt$=4=QtSz5Z1dlDxWSD{Dawv+%P)T;7t{vd32SPV+wC_#U&lxp_kL zUzNZwp?Xe*SS0d|V0DzNa(^cT@Fcdd&^DCjE1hrucNs`DkyKwBm)#{6iAo^g^!}i( z@Y4;Eki_`j*2SGe{J$zez$N!h4UE;B5-q8G%=)aiF*o&A{hC;LZfN-2{A+Ug*t@B) zk4qz@|Ef&v$GkvvH))N$DCfNWS)!KU!%0f9aecK|;!u%+cS*gEa@X!)bzJ8!QVxgBKQAwgviT`iK zKz6QV2+NC#wdb5knJI65&3$5irchD6Vf_}ruXf$j_dwm6;qAT4xnr}nP?4XqaE{%N z)!DBPWOdRPsu6GZF~KEAKa!kALG zrXRMm=X(D!C9lM6%TO-%?UAwp4Vv_&t2Mv=;%JO%zQ<(yFLcCk#QS?Dn<+}cLHs9i zIZc~i&6#l`6Z1i^`h*igfp0SD&X9Y$>_1+Yv^mJymQ?EXh zVT|Xs3QRGe>O_47qzj#;T!S(zP~=dOS(8F=g8(cC3rXE5*PXQDa#eGnp(cj)s@a^%=bPDXs_Go6{VFMYg9UjE4E$6}grmb;;R4QWRa;Ye<*K?!FjSkV z8EotVS7SSOj(?{Dpi$(gV8-cWjL&n`T)g@^QdS>{&5*)j$kyunyxO&9%{fs%f+h1} z;nOYm4yY^x9oV>eHZ*8tmLoP3S3C6!6~wyp&Z^ViTicE4#rj&w$%g6AQAjWpd=-jI z@q2xly7T(`iPfdGughGnHtTCH4F*aWuu}0WJUz`eOZ6X8eFNJvXsEeml*HCA7~&aLNjd04F;}WJ$urc;QOWO9Dw`Z({G2GKp18p^k~!$KaQlq! zLBv4x7dkj<*E1q-(`@T(7s7?37tg;M^dy)mWNa}zZLmNI!x`v%4G(Rv`Hu3tE4^d^ zm3^%QCCVqKGVURGqI%~PA8qabV*rFp81sc={yO?GHE%SEyH<}$fqZaC{EaSla%Q$q zjsbC-aBG9R=W}r-tFqmc8U^5UYrzHcCiQPy=MUGv6bt|mJM-~&aNen#5qHs@Dk+bG zkxx!3bV<$@Yky%7(&UXxS)YLkDoYC~tIrrT&{-;C@%sq;l>#EkX9o3Vtw`R*YRWu? zF%U6<*%dk=`?-Of=VmkG1n2bUVM?vUjR?}RYA#*RRrj%_qxN4J0H}(4TlbGKqmkP+ zUm1a7y04Tp%c`@TM;sKsfop#RKU>UXJuzOgnu)q5n(>xICguDe1f(~WEFZMy!5;L+ z3@F|KGaS8H#7Nz8zgMfFm#ORZbyKoIt|TO|Rx6dyG4`$#@ik(m97MZRqXzgD&6%&{t<4+sF%)4GLy)G}eR;?Q??rptD;c*#G!tG}(}nwco611U@XVkoPrQbt+0&5ra}hW9>CfXpxN zzcyHb%5=F!X^ZQ%oY_bE`}ohCTfduMzRKpdcJ``PtCe_q1uwElJ}f8-x=5a5~h z>E_;h-wL!}KbQia5>l!-oGQ4WSX*(J)zbR z?pHoS&*fsXEv%V9LkTeHwkAk0pt7c+gxy=g@0sm?XCiiIert%PT$t?51gZd0=LTZD z3Q-((cCfD0xkX(7-1vzU@s-L*W$y7Pln+ro2i3??GGdJuH+AV;y%x6mI}_>65lcz- zwn1G}CX+FBZgG{GF-J6Ywa{pb_m#h3#cLnmmn8(*1fva>Da@jJ3Us6)Wp!Z36B2&( zea!lMnfTb+Fh_Rg?7$6{$z(?U&KQS0N{>Ai!CAPdEn^y%{k|>p3rtMe;?|4$)Ce+j zJq-VPx@>j*@UP$*%^$TS0$v3&0s#?WCF#lNzf>(Y}(C8RIGE7odHdS&lBN!|S zb`DPQ_@{XQJb1MT0R#C2=yw*3_O`~Pj(Gwg`MD9M1|rdCW0-!U@I}`grf!0HFC@>H zpVX;yLg9<1fs4bAssF;ZTgC2Jp)+*_JLzst{Qj9+NbL0JC`nL)qn1MFbG6UDyAj`f zNTVW41#wM#xog-%2rK#LL4XaCg5IFGYl^ zF$kWOn!LK(jxP@`?C&f6YAWys()KM~$kwDsqf@g9Q3++IAs=ITNUf%@#&^4qNaSC7 z*o(GNk{Q_DlGz+790s6F#^lb-W>wgd?~-~GB|&|m<<9mR8Sp62xeJ~E-1TY>8v?@A z>qk5~X932q2&prX+tme=+>m2N4K1&i3yM1G*AP}7`G@x)t>=2S#SLEZU*OJ`=SMy!RClL-d&3plwd1`cr%B~{|sV9-2ZM(YZ#r3k@_JXvc004Zy-)kbscqlBre|j*mfv3BoddO^HkrE z!qhCeSMp|Yr8&Xmb5u~}r`C==N)dhF>>DT9&m#eJedhYS)9`aB3LsqD+*COlWJ3ql zEHVoD&-aRlExD)hrF4>=ue94=^rTtRu)B$Vppg#tHhI3nLql3NUSk4#K#029yHxuE z-tqGerva1kah7B0)*q8Q7fDry)Qa-AsWNM^Zu zT3E|A>oy#0Dw_t(A|p=W22_d3%@T`Oy7k`F#N2qiDt^$`7CjpvG{eR+$F5l+Ul3E8 z)0^@}w(|$TkvU!Ij!-iAyNf1PHBm0oE^(9;>*0d;jrh>urMiRg#l%pZutC(cNM$Kd zNcJXKj2RM8@5#7DC2Rs{Gs`%hlfleRw(sO1GlsOR=V>uij3H@$(?o$y-K4L00~H{3 z?SHJ16?1MWmWvtFyM62L+Ht`mt0vWx$omZwj_b<=ZL(D3aeXYTSnf8KRF5VR?iJjm zoYP{*N9yhUipeZD==GP*H4CLWyllH%R2WeSw8_#7M#ph($kgj(6Z{ImghZ}kkA#;< zFh@@SWj^XhRs;EFRmrB(K%`v0NGgm7$-?MGn6Th7*E|~0Fw#x>nw;>FjF1!_*M4m( zwppCpiTX5REpS#kLx=GL<8Y1ft&Qwa1A=q>Rze!L&?MYm>I41}!=pR)2~1~Zm0c@4H1Pz;(MPJyPEc)rvWbEiZP z4=TwyNh6tqz4(MuUsJ5h37#}e^i-4kz`~ZXtbBs5L+uIRhjocS$SKXk1wGCG#y26m z?wJgXvSaayEBXl;JoUTrj}&(jvt(Zws;z77qXt)x#6KbfGV?1v-0}MHC~y7fNLR6o6Jm1u=w=uXCgDu8szk9GA_7NT z=a)9)FSJE0uej&A42Fb?3*UXE#|7&I&+^T+xQrGENb|tbr#O46hq7iQ;nS6 z;g1muy#FZz@dSupFS+gvWEd2t`$BKJDx0As78l`=n=7%ZKMy5#qHIgWv$`Jza8A?|qOQ+N@9N1yFfsyUcdqiTjf5KK4o%k#&CYWzFi( z#ZWUsQM6=IvItibe>*U3sI%k=$r-!?V-#l%w5)2BAXBlZ_O`a5n<9IZkqSog)XDi` z><}<~>5ZEbF+`g#CeI)Kf=6D$M$1f!r-tk(x(lg!oc$-R#5RIs1M2BfZZh$#_q1va zwCLugSY{`ZQrRuGM2SU$&z%A@T&3E{cZMZ((^Tgpr7bIIHzH%W;WGt1zL8hQTSkcP z^Vr_crMWDGAZl*l)A(Twk4(+`ajS-&Xrx&w(N8qm3~!*UrIA&L(zxF1riI=GM%N-` zNEXM;%%z2ihR<3qavK?=24H5FJsk1QLj?1~)MU{U1i432+cF8!aKZM8-M^RzAt7Bi zaE^rB>k$~u%8L+&3(e1SRio2}*Bi&V35p#~a*65^ksUqpHymo` z8KR44eG*_-WB%c45O7e?5YSM6%;SEz8Wc1TgOrR-l~vS;oLx)}6N^LK*fHP_hlBjn z;a2NKUFcG#?gK$?BBm>jFtBS#sL#F-DrCM>^fsXMP;xUW*6{rr2j4(cqRF|Kc6>t7 z$uQOXSGJ_lmbF8R;isbnWhOoZfOM_tOU)15g^TdZj>))l!Z{>WPFQK`<}e4#-!sd-}qhmtaTb_f*@xo zL5@Hnx=&sbjOUxY?)Fo-qpDag&!#MpA8u>%OI6geIBxSl*)7n?lwq0k7xy2^eWUn_ zAIXOh`@wzXZQRQu?mg>5tTC-Dj)Z-Pw?T0_4&CxX{%qN!w|kt9(DD>jut1(vy1jtz zMmMqk^nJoM97&4yT3@zKUL5;gUfJca(5ib!JAMZ-C_#-e*sktubU|>JB7b+i|Mt58 zH=dvv$84Roi;VdcMQE|F0JW`ipZOGAD7814b8zfLs0@PYr4qrqL(p7Z#|n`dIBn)} zfagJT@GZMp`e@b<+y^<`L#~Bo zcwL+g?#b5cZ;v&wHG4z5&?Vlt#P=9rhFzCdBu~hozLhhnZ0GTd_|B?!!~6s=r^&Mg z-mlqH8wS42eJISwwZ&p1l1X&srxS-<3MP^Ag4feRz32y6X?_^LoB)38DmlC%;NdO( z<`jz6gD3AOMZP9Qzq2m;wxYy5B9?rps|O`X1tG)7aqM1YyR|@FC}m$X&$YfwlFQ(ZBQ8i$i?(3vAwdzEmhn>g7-DZ(fZbd41X=3TH2U;PYZtfRBq7KPp9?31GHhm6*Q z=KGqXUPZG;rJF;IxvyqPO->}EfsaXK?m;^lc;blPL)5t9FTLj6=*=~1#O#suXU)Sj z#aIwqsJMK5r7C^$hp%I~vTxjh=lC_jHRDKy#{?LgOn-oT5<`;NR4A4;zIuAyc& zO?l@FV|lVQi)8=@g9m%DqGA;{mwc`UVv~l|$5qzy6(-H;vW(Kw*Cn662e9D~);3ta z3z;LofO_>xSJ0q^>g-6dpnk<|1lTKpN7WUgL9+l{DMW@VZZ5?x0E zY>d(=YKDpNCPgQvU*znyxDcdkw8K74aLiGPV$4Xnp?6X!rd_NW?%#{_O!!jIG+4Wp zh9~H2t`|^ok_LT$85V}42q<11ILx4{)cQb22)T&RnOAl!HZ<3)bwhAMa`;x>WP-0R z(wKft$yGDEVka=eyMOmNqV?yfZ{cLqh4>7wJg(lIz;Mx2S8J%#XJQ_&ra6+xt&cn5 zwkOmUBgX(S+mrFXE*FZBJ*4WkH{;CERCk9Sqr&{!q#cS+3DT1 z#DE@L7Yl64_qapzjTwa}DU{<5H4M z_J?v2k!~U{>L!? zb5aQoF=RkCF*RdSRd8fsHF{UwF>x#!f3otQeGn0{C%`nnfSz-D5_7AuJQ^HH&CNra zHW4YM&mGD@lEkHo4hK0m%K*IrbngK-?aCY^Ros4*gR?j)YZiEyz=A!QZ8*GLAq32F}^AmU&mwA-yWC-B3Uc$=%5vB_5+YzOlqg z3$f85u~_#z@KI{coQWaRV|BF}(YtaMu04{+$6+E=Gn8f$+Nd@dKeHU=m%+|~*Nt9i zi)<2ycAH%LtK1g-qN{wO-f19v0kN8~+1!l_$c`#eqG3Xe(xNnA&& zaPPpHS-S-#uRI4sSKHUjKIOe@l@j$Xl2FphCG(p_0R_loLSW=7roEN^+vHEECMJ9ary*_(OQ?t!7kGt&;X8&&eIl$hxcs!oZ)lc^d72h4#2G8P z2(R_;yJS`#yzmd5b|N8bXq5fzeew=#7xK~9b0!Os><=Ny^_+a92JS~Na+hG+!bM6_ zAJWdZ1ZDX_FDr9qyC?;o071mrZx?e9$PBAvmD@TZ&vz!!)Lu~ST|x%f!Oz@BBp%Vz zvrOZjur2jI0h}5y8ZX$lluufpkw%zWw3g%a>icbSpinC6(&=u9U{s%bPK-SPbnO`O z^u9)+Gj8j_QXl6^*4q0Vzg0z1b{{{XB@@31#t#xOrf^Ib6}+QF(^>>AnoJHFejJ(k z{B_fhfS*41dJ2- VThTl~h@vHODz8)HF)dXk@dy@65M3aeqq-lELtY+eOpyhfXP~L2S2| zT^uO)#Gm{1v@j2DL-B=|zpJhLL)(B3mI4`Mee$(<{WR$9H(>kAmm##0+{n!5BxAQQ zwgp2Whx3KWj=G}E6>!8EX09U->>D+x#7qZs``kZE`iv`mT^KI4qk*Y)ShYjLt#n`@ zJeFShjKNXM6VsaDBae*`ZiygDI1&a?SDLcHZi^nXIATk%=Do?uXaGOw&vE)q{GByx zG+G=wA44WYg>_?RWD_Qk0M#!XMRETX4+)kIj%#|${26RioEZHRAP)`BE(f#Fr`<+o z-j>%&8l%9Hg~?~_glYF!K-pZ&00g0V(z1a2pr}Vx1Q-5}(J(wK^d_p!E4^gZEp6BA zD?(ZfdxOrobnqR%X;6z4(uFEwG-^Y@^P2%;>9%6h`|u9)mgdf79$XJ?eR~e-8Ga=r z@!pYTZvF2$2BY5`mU~ciDBssm+OA9$7IrE|Z!+q<8TIpva+AfiRYt&?&cY@Q9hV%iyKihg9*$;NGYnd z#7`vU8mvVwQXVrm2_h^6_K6zpb|)=T)%IN1(?#dKG%%mSYQfM=^+9(r42C*&W;+aS z@-RQ0Z z5G)eD=7IE^NaQwgg~)|CZw#h2u3;q$DD-~X7l(OOC)#O~>wS>09qjog4vrY=k7|PB zI_~LaPXK5EyFy^CmuJqdQ(!D;-EepoX2_qj**jPW3A~?mhBQ}}r8X`Fi|+Mamg+54 zqYTsd=IS_P47T>BowBD<#fUp|r=DjDk2V4K2~%IqWp-U5A`Z-WX245vq?(rKy!Bc} z!4D=F^eE6&R*qrM1$TCUW8vZgn?hm%CQ zqZCfiG#e)-H^O;)U(j_5<-IGrc()Ckg>_mf=vlznxuJ{$DbW}z z$Ju^w=v|zmEB_(|EbTdQ;<-)-iCKLDMl45-?bJ5t3v>PzA1dniQ#mSLXgFoYK1G^hu z!qxR`kesDWj6VRsOhh1;qabG~mI(qyXkK7rXp#;KF$aYk<1?~f=89G?#KigTXx{Rj z^Rta(8pJG4y33reG%{lg*MKh$lH4O#|6xba%xj!1wDHo;n~|@X}=0YgE&grW!|= zcQ}y*aRt!?T1x{5)@^i>td?XS_72SyQQ?JyA7Cjlx+3s=y5>vKsNqG!3KsRb_M`9& z6A54w(ZL|*cG%Xuf)#v}ryUAhNvKO>!${2rw9n^Z0$4_|RJ3tZX#84Fy^grlIzRdV zc!|kAiF_B2ARY+Xz=oL&?1T<>DOjMN6uB5{rQZHpRY>=DBr$lem+;l{#pkM@V;)8d z5lugDsPBi8N#SI@@=D_@tt`KMB{6Cw4h$;@c$T-V6N$c*q1n`slMS18PJpZe&al8B z4g_TbQ@yYe2n-fN4W7%#8hqX{e8OJ01C6a1z7|X}NhwQ~gJEBz9wzWe8UXn`?A@2x z{FdT2@uC~u6QZt3A}NO`jp;sdT$EPg-dt@H7UMc#Fq#JZ_U;+`=hNGBO@ZZmao#YT zKmtv&9ocv>jxd!svx(Xx1i>Pr6Wy=zp}EmSVNVND=PYX5Q6W2 zfB^Wa-)q7-D)jyqg?pyE3IRa zxvvI?c6`+ifc#fQ0HfO+1S^34-$C$O@YcrvaB-;d-zY+<0#J(e{~d%5E3Yccg!KPc z6j~(|IvK7m_g_J}BdEwEuD#iR10mv^6R9z_nEvI)AiI$z7OhDrkCRxI|36h>?2itV zsYC*43UT5)y;=VO%N`^1B^aIRywOZVlAiw+gm_kD{$dXFi2+EByhf4N{tJp4>)*+) zNrd)}hk;@Q8$st6P}4Ez`9?58CU-?PW;+~%>MtNNyT(fyRUep*o|;~sp#-B}KrG(c z+BK&#Cj{{6YpT@=ga6-AsO3li6E%pHLj`<#?#HbBcMJf_5Ak_ntlR+V>NFq&K>ll+ zLiZ@j)+K+~hZmczFd)pY`b86;NAh(M6Ov?M1ymM-anCOp04B8N2@t@BJISvVvyh1Y ztDOL7yO)ynqH@DJ${um1^Z#%Mh{;?L(_)w)`bsz_u>${Jc>(i^FyGc-sYgU!r8GAG z1B6&IB}>(Wd1O`*7gW)k@N*a9$JrL}$1Vg)rpg-qG;_4Tz1*Qo?2)Xh`*n1pIOE*I3y$lY6uGsS8JMCI zUN@=tes3OWU&pU?ZQUDWCse9`_rpITUJ1Hay?gQS!n?W&arB!|yI-g`H_;&>E)t=z ztOUd(Cge#pt~T$t_iTPQjA6duU2+`T*{iCcV(!Z=8me9qtE(KZrmphxCd*dX2Nty% z$D4I4%XPjlU+%t`beQ+-&n;;Wz<%>S!KZn>xa5`M;QRiTC7l1W4NWB(Nlt55JK*^I zlt3)!T5lY;O2q%l_B;4a3HKxHcQ1eC%G9I2J9Mg4i`PnR&NFBU>|^dGa zN+Qf4+sTNI8nu#-_hf(Lzo6~ZXqIppMU}I<6lJL(^BKH;L*pUtu>9r#+55sO5A!{) zQ@Pps$0Ne_BE0X3Z;H&QmumO%%^<=Ayd`0Y;71F*EDY5_(=p$x+r7SBM%XfS1yjey zqpqwRH!6P>aeBmOiNIW%j-H;g5D-e|4LKvdU>@dn$Y!3HcK16NOWN6$yb zr0iuXgo%~n{pN(`)DH_}Aj)z}=j3CX^cKXS0GZ0|@kjhl>Bk>C(U z3)`;wHf8f*h&f&OOf0+8>n7CuaHOq&$lktNhU+4g@x)I-<87{juuL5b^L-Yl z-#7MIENX=@*a)z8u}^VKajIG>B=PqNx0+tTr*0_tz*v32EBH#e{+@5Sb|;6W-J&~c zi`TZW|Cq|H;lfQCziaeZCWy&-C$&axeJldQ>J4La+u_R#b3>jY8=={ZF|-I`BqsNr z2AfvdV;>UCXpE~jZEWT^JoZYdKKPl8>#=$i!tYjN4Gd8}CNfXHU5?#QUS>?+IHu^R z-F4o@TiH09U4icHn-)hk!0nI4xA;=65EgdK`mI-YaLDHPRMO$WjCKb;V&#EeAcwp2cfe3{iZ|f@&hlu}rdiu&e^lPVa8RRjV2=u=0F|-wOUWn)@ zlG5$<;p?n~4j;wtF}P*E>AvY0V}J$n*zzbU(j4XS*eXTvx5C@xEDm9~&{J{PS{q*< zRyRM1t2>ZhgLoF+P&>P7%4VMdb9$OyydqKn!sR z+e16l8?fs}Cu-qXzDDmQMotY?pga}~kL4fBzcL}J?YfTT?Az;tWn))~ z*mSN{$>TRovFKS|-?&;nc5oP5=i`3@EdMxcVMPBCdceaP)Q>x1KVs6);KN4xNAPj1 zPIxyF(6MrSBKqIsPH+rb7hwC|atLKubnlmbbq+tybY6*eo17MdeQlzXaehog&d z0#?$g#CAW8MYz{K6tU~8j>T=Ju)&VvhZ|#^8dWV1Pk>>eZtS9nZgRb6D%5Q%!BDVi zTP!P#M50)o^p_E)i%$T3!pv>a{ zT20_W#RB~FXM3QYO*vAE^pXjT@={%yMj}wAdVJssAWOZkbQd=?s^*UL&e=m(WOH;` z)cv8h70(v2f8#2rhTj%yzeDNBFDK|(j&9pLzp5i^^yM|{%n|lOG}RF7$~Bsua9Mpt z?!(DKu^RKnXkU&4Dn&>J@pYFBfGIjJFfDeLHF!3wW68@Uh@t|VT-ia|vR8E#Xe10rL<2y--AK`zn6ldro$#KnHT zkwwj=2kEo`ziYXPdQ9UfS)7q=n#Y!gY&1NB&zV)f+|?u6#_$7sYPnYcmkt)i9)W4arG6huS{ zu!tM@Ao(WsuC!4j_XurtfBEfC$7i28Q;`i{EWmC^8(OLFAz&foLu#(21&=SxSk^zg zg-L}SIxcZgXBQh7&s#IEUPNcsZM-8F?4A}X!UO3OdGUml9(TQSaB@MG6v%hV#d{Hj zR^||Io$eI)+RGrS54VebFK-3;8S|Fs03ekan=g}dXi=|m26^M$p1Orj&CyV8dPu2T zQof`mBa=GAe*q-*NeC&G1;W4kW1=gx%l$M(78RW^0WRrclukP7aQ3LVFP(#@#8YM_v_ zWNTyavPx9s8Cj(g#(rWLH+|#FazX*&b+dbia%S4<>L{-m)S;sCh<)hAoGlX+a{D$bIWr_wN z1EmmdFNj(inK+0Dv4KaQ0$!rvWVTMf={1eq^s<^x6>BGO$Hz%sTkOrav%7?tfDs7% ze?GC?Z!xXjOgn3R;?l?KMCLarjkTpa)~{_MY1rQ1`P+_}x9Q(5ac z50P=`EW;&JSM;V*v-DtsD8ab6%CxK0mx>;D@yZ{bB7yHAsuEJ8Bj|*d`|%8VcXTqL z+M7}+kyQ1A%o&=sgNJd9(pXh@6~ah8Ovq5tL0?@9g239HhzV2?*+n?JKqxu3^g-KI znvACFCfeXJi%6f6B{SiMNE-R!hbsy}f>a!smg!{zSOzCS=-o zevK2)PX|t%Jsv_HK0+r49cY|N)8`$jxzM-oY_cD|$x#Q22At~UL16U3cc})>_>#=j zDI*gV7>RY1r0mDN_IYr-o(uf;*wXibDdE_8565K5gYN!}g?zJ`KumWI4Qrv{hQ(>w zX_c)E8Nmu)d)F;1;QQXuM5NGdrVj2WK5|FyXxkM3qea0iwDes zAvR?;;u-c4Zi-kagPIi;#7L~=hv*J=AB!E0=M6Fk*dmuJOtQqn9j=}Lj*O7=Xye(R zIiLov?%O?Hl%ppRMAg?rM&8b|DhAH`+4^*6MYHC3#9|c)#Xqw$^3R>yR9ohyo@^T; zSW4nC_#DodjP4b4c7V6|g22liM3|o+U+_?hkiFFCC#6mJIw01diaJpz%)X}n;i0vu zr>czOWl<=qVw;_yzR$wEX)Or_LtGA$Bl}UhuN{_`<2CaA7~K1)y=B^*tunW}N17da zAkcUWt;ZWZNki1u%8#Tv2u<-W=M)2Vrt5q$VXP{{R3{JV? zD;o|4r2WTsNq?M5jCBAn`z>$_O}qd}5hC#Z@x%J&qspdkGu&KXNpfg+tBsDsURw0? zkfGf}QBm=3hX)sQ0Frk2$i&CTU7ciU=4c5YT5QN5P^G}f`MmnhQ6a13=l9uc`? z!-O5RvB$K$X}ypIyhi41iLkCZ-fhJQFgP?K`UCnzMAyO8FS`noKKRdt>w(}Gsx)IU z`1yu>()`e4-(6{`tP;P>8D@C)1PDYa;PT=%ZLr}FMnk9dXOm$2m}9R}{=Jp423|(z~ZxEc?X^krv}30kvK$cL96g7X16h-acqiS-JtYK@l}IzZ_#}u%=8-Tg_QS( z;V`5x^_nsy=UgE3G;KN&ORVU=(7}_tp>K7=D|hU-y5qiNQYKNSvcxvB*1JlTtH@oc zbw6Q&Qq@zBipOcrfZ;Mi#oilkmF{RE@XwYWoF0F^esB@8eT1!DR&7S`(OF<|ls2VW z0cS27+)i};y}dUWSQ08Axf}xm`rkmClsLSO53m1p zRCQBK(~zmPAgBic?AEp+mn4wZhV@P}YRM{pAjxpAROqgwl96(eTMqXRA=ep+Vi%bc zP-}lSl6e^lV>vN!LJz@2-hVCN-S@H_CtcbGC-<9uoRv#p5r71EK6}ZE2@yUUbDgG| z&TNzyBxOY06j?<--ytALIf-*Y&$<&+U3PM$T7yd~c7D45H9tPOusi8P77N#dbb%ik zkSZLnv|rG&2~Zkgr8zJt^I8W;Y15QQj=REKc2Y>ocP~8pIIdkvjV5lASI@R=xDl048KqPKMy8o8lgN{$*L~dA zjn5nuq`qNUO+AQ1$WF=MDJne+RgtYJW5)(>@9j4uD>^ZYM8RRb-w3!qL;36ql9# zgA>n0^2Mb$bU92+0%rr+B6LR;T9W%#Jv>{9kF&>-unFB}7N2v4j^CXvYCx|Hgm3ql zTuOxr4B!zx*g_CPBD7@j&Tjzs8sdxu7{arKF-RfCc1VW z26|3?>D9Et>1pFt+xL}kvxlE&Sy*Hn$$4YQpwG^G4~=}ZE=i8Ihb|^wSCTy*p+;X2 z`AP3C(-SGo%5{h_$_}6Me49I8rQny>tX~MR#~TdTm%;i3o@ZI+Ex)jEO*7&IaZ92% zRczJS47;2iusS7s(_A6guu0`;M@XssTS^i2_MWF(g4rxj2T{_rQAtC%i{Vj z25-4Tf6GUo&hsop8NN#a`WMI)#`TR&xTe${&8B*o2)4a5nWA#ZFo|;9B$p0NL4}<` zAlY3&_FPKulYGxetO6T8N#+qtsUeAYB z8S$d!O^k}ZMt*#yyQ(BasCW)*5pEI`p3qg;#2ceJJ?`CiLC=i@N{PhBx5tX`QY+Xt z)XA+x77)}|6~6a9;kz4SY>d*~%ip-Z<+wa=!w93_-c2Jt=Sx7)Ki8P7GYa`A z6GzhmsWXbQYmTV6PM3C`Ti9_aC=V@U8R9iKz_}Z=$p@f~X0}uJAMAy@fGNN-POS~E z35GDATS7i6WZl*h#ovxb=Fm-5h7}xCdUGD#mJ^q6O7vz1;eIf4iB+=DI-^kQ3v`JV zLw*LzumgH*53G~A!}LVFGU0gyERUkmTcM3l%F}_vslm4F=o0XQ+;PG}qyRpmIM_|R zJ{}t3CqTIImwyt*(`{+1sR)IKreweZ}OU<~$5;e%=6jsniCO$Y`}ii1z&(niKF1 zEo}5vHCPX&bR&UwiHK3BKzbXqo8`yXtqFlLepx5i5%`oc77; zqyEh)@cYDu8fi__M(mF{nEb^jaFTVfx>jSHOx|K|&oQU!P%*6}tBY|c7uZw7Ke?o! ziPx2N6$-DO?7yaLu_U{EB(WT&&MPV9y+G-TjaKv8;DYTU3n{jeUJ?CLF=h@E%+0p# zFRFlG8~}3jQV4Q7!F|t0{(l8}1BLvBma=F*41Cwpg8=C?lNT;u2WQC@9yQaqNGRyD zDd^048KwftMJOgL2D@0)Tpx zU4zl%>2)^-eIed8)1srOHjjz8wHGi!QCO5(Du5)#%X$Om7GQ$Q9rV@a5{N;IaUg@J zTxWmgKcBzS&-pbtm^uTfG0d?qF^1u(yul(uWx#UQj}S&SBd9b?$yE)_5N9&#F!h1I zmKml4le5Y6qDg`8y7>I`k4Qk8mmY({X)Y+L(*`9&2Fv02C8ptKE2wZo#1zGInQ&z{ z(IP{Mbi9`-N$mdsuj}bNod?f|=USAi2qB4b=P_fj7X476BMgE_dd_j zuO5EegYWq1nwmq(#Q*N1DIp*uU>w7 zQzehMf6G1PieRPfHOs;*OGu#E6QJ<|RZ5BaAUTn*-3}l3Z=!c@yhrx_CT&WkP=JCG zC1ptX1+yGsto5~yw-g+PJQ;+aK6yKZe9TbDLvo9t`(eg6PO56kbh^ZAa-=qr6? z*TdZ}E@o=vK0;|7Kl~s6*{4Xkh5!Hn literal 0 HcmV?d00001 diff --git a/Topers.Client/src/app/styles/index.css b/Topers.Client/src/app/styles/index.css index 647a824..03afaff 100644 --- a/Topers.Client/src/app/styles/index.css +++ b/Topers.Client/src/app/styles/index.css @@ -1,13 +1,4 @@ body { margin: 0; padding: 0; -} - -.root { - background-color: #E1F9D9; -} - -.wrapper { - max-width: 1280px; - margin: auto; } \ No newline at end of file diff --git a/Topers.Client/src/pages/MainPage.jsx b/Topers.Client/src/pages/MainPage.jsx index 0d71bac..8cc71a5 100644 --- a/Topers.Client/src/pages/MainPage.jsx +++ b/Topers.Client/src/pages/MainPage.jsx @@ -1,16 +1,28 @@ import Header from '../widgets/Header' -import { ChakraProvider } from '@chakra-ui/react' +import { ChakraProvider, Box, Flex, Heading, Text, Button, Image } from '@chakra-ui/react' +import HeroSection from '../widgets/HeroSection' -function App() { + +function MainPage() { return ( -
-
+ +
-
-
+ + + + + + + +
) } -export default App +export default MainPage diff --git a/Topers.Client/src/widgets/HeroSection.jsx b/Topers.Client/src/widgets/HeroSection.jsx new file mode 100644 index 0000000..905d404 --- /dev/null +++ b/Topers.Client/src/widgets/HeroSection.jsx @@ -0,0 +1,47 @@ +import React from "react"; +import { Box, Flex, Text, Heading, Button, Image } from "@chakra-ui/react"; +import { FaLongArrowAltRight } from "react-icons/fa"; +import handsImage from "../app/images/stock_bg_hands.jpg"; + +export default function HeroSection() { + return ( + + + + A reliable helper for your plant + + + TOPERS is a manufacturer and supplier of complex water-soluble + fertilisers for good and safe plant growth + + + + + Hands + + + ); +} From e9e27e87962933b3fbfbfd1ec4092f1b05d0378d Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 19 Jul 2024 12:03:01 +0300 Subject: [PATCH 22/39] Fix a cart. Draw a card design --- Topers.Client/package-lock.json | 100 +++++++++++++++++++++ Topers.Client/package.json | 1 + Topers.Client/src/entities/Good.ts | 3 +- Topers.Client/src/pages/MainPage.jsx | 3 +- Topers.Client/src/shared/Cart.jsx | 50 +++++------ Topers.Client/src/widgets/GoodCard.jsx | 47 ++++++++++ Topers.Client/src/widgets/GoodsSection.jsx | 29 ++++++ Topers.Client/src/widgets/Header.jsx | 92 ++++++++++++++----- Topers.Client/src/widgets/HeroSection.jsx | 12 +-- 9 files changed, 276 insertions(+), 61 deletions(-) create mode 100644 Topers.Client/src/widgets/GoodCard.jsx create mode 100644 Topers.Client/src/widgets/GoodsSection.jsx diff --git a/Topers.Client/package-lock.json b/Topers.Client/package-lock.json index f578417..993dc94 100644 --- a/Topers.Client/package-lock.json +++ b/Topers.Client/package-lock.json @@ -12,6 +12,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "axios": "^1.7.2", "framer-motion": "^11.3.4", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -3025,6 +3026,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3041,6 +3048,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -3209,6 +3227,18 @@ "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.0.3.tgz", @@ -3398,6 +3428,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -4082,6 +4121,26 @@ "node": ">=10" } }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -4092,6 +4151,20 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/framer-motion": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.4.tgz", @@ -5070,6 +5143,27 @@ "yallist": "^3.0.2" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5444,6 +5538,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/Topers.Client/package.json b/Topers.Client/package.json index ab32b80..757a8ef 100644 --- a/Topers.Client/package.json +++ b/Topers.Client/package.json @@ -14,6 +14,7 @@ "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "axios": "^1.7.2", "framer-motion": "^11.3.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/Topers.Client/src/entities/Good.ts b/Topers.Client/src/entities/Good.ts index cf8ef20..fdf49f6 100644 --- a/Topers.Client/src/entities/Good.ts +++ b/Topers.Client/src/entities/Good.ts @@ -1,5 +1,6 @@ export interface Good { id: string; name: string; - price: number; + description: string; + image: string; } \ No newline at end of file diff --git a/Topers.Client/src/pages/MainPage.jsx b/Topers.Client/src/pages/MainPage.jsx index 8cc71a5..3e3c4f7 100644 --- a/Topers.Client/src/pages/MainPage.jsx +++ b/Topers.Client/src/pages/MainPage.jsx @@ -1,6 +1,7 @@ import Header from '../widgets/Header' import { ChakraProvider, Box, Flex, Heading, Text, Button, Image } from '@chakra-ui/react' import HeroSection from '../widgets/HeroSection' +import GoodsSection from '../widgets/GoodsSection' function MainPage() { @@ -18,7 +19,7 @@ function MainPage() { - + diff --git a/Topers.Client/src/shared/Cart.jsx b/Topers.Client/src/shared/Cart.jsx index f0a934e..fc8c0da 100644 --- a/Topers.Client/src/shared/Cart.jsx +++ b/Topers.Client/src/shared/Cart.jsx @@ -1,37 +1,29 @@ import React from "react"; import { CiPillsBottle1 } from "react-icons/ci"; -import { Text, Box, IconButton, Icon } from '@chakra-ui/react'; +import { Text, Box, IconButton, Icon, Flex } from '@chakra-ui/react'; -export default function Cart({ cartItemCount, cartOpen, setCartOpen }) { +export default function Cart({ cartItemCount, cartTotalPrice, cartOpen, setCartOpen }) { return ( - - } - variant="transparent" - size="lg" - fontSize="25px" - onClick={() => setCartOpen(cartOpen = !cartOpen)} - className={cartOpen ? 'active' : ''} - /> - {cartItemCount > 0 && ( - - - {cartItemCount} + + + } + variant="transparent" + size="lg" + fontSize="25px" + onClick={() => setCartOpen(!cartOpen)} + className={cartOpen ? 'active' : ''} + /> + {cartItemCount > 0 && ( + + {cartTotalPrice} £ - - )} + )} + ); } diff --git a/Topers.Client/src/widgets/GoodCard.jsx b/Topers.Client/src/widgets/GoodCard.jsx new file mode 100644 index 0000000..da30e53 --- /dev/null +++ b/Topers.Client/src/widgets/GoodCard.jsx @@ -0,0 +1,47 @@ +import { + Card, + CardBody, + Image, + Text, + Heading, + Stack, + ButtonGroup, + Button, + CardFooter, + Divider +} from "@chakra-ui/react"; +import React from "react"; + +export default function GoodCard({ goodTitle, goodDescription, goodImage, goodPrice }) { + return ( + + + Green double couch with wooden legs + + {goodTitle} + + {goodDescription} + + + {goodPrice} $ + + + + + + + + + + + + ); +} diff --git a/Topers.Client/src/widgets/GoodsSection.jsx b/Topers.Client/src/widgets/GoodsSection.jsx new file mode 100644 index 0000000..13bf915 --- /dev/null +++ b/Topers.Client/src/widgets/GoodsSection.jsx @@ -0,0 +1,29 @@ +import React, { useEffect, useState } from 'react' +import axios from 'axios'; +import GoodCard from './GoodCard' +import { Box } from '@chakra-ui/react'; + +export default function GoodsSection() { + const [goods, setGoods] = useState([]); + const baseUrl = 'http://localhost:5264'; + + useEffect(() => { + axios.get("http://localhost:5264/api/goods") + .then(response => { + setGoods(response.data); + }) + .catch(error => { + console.error("There are some problems: ", error); + }) + , []}) + + return ( + + {goods.map(good => ( + + ))} + + ) +} diff --git a/Topers.Client/src/widgets/Header.jsx b/Topers.Client/src/widgets/Header.jsx index ce1184c..e8b1134 100644 --- a/Topers.Client/src/widgets/Header.jsx +++ b/Topers.Client/src/widgets/Header.jsx @@ -1,47 +1,91 @@ -import React, { useEffect, useState } from 'react' -import { Image, Box, Divider, Button, Flex, Link, IconButton, Icon } from '@chakra-ui/react'; +import React, { useState } from 'react'; +import { Image, Box, Divider, Button, Flex, Link, IconButton, Collapse, VStack } from '@chakra-ui/react'; import Logo from '../app/images/logo.svg'; -import '../app/styles/Header.css'; import Cart from '../shared/Cart'; +import { HamburgerIcon } from '@chakra-ui/icons'; export default function Header() { const [cartOpen, setCartOpen] = useState(false); - const [cartItemCount, setCartItemCount] = useState(2); + const [cartItemCount, setCartItemCount] = useState(1); + const [cartTotalPrice, setCartTotalPrice] = useState(100); + const [isOpen, setIsOpen] = useState(false); + + const toggleMenu = () => setIsOpen(!isOpen); return (
- - - - + Topers Logo - - + + About - + Goods - + Contacts - + + + + + + + } + onClick={toggleMenu} + display={{ base: 'block', md: 'none' }} + /> + + + + + About + + + + + Goods + + + + + Contacts + + + + + + +
- ) -} \ No newline at end of file + ); +} diff --git a/Topers.Client/src/widgets/HeroSection.jsx b/Topers.Client/src/widgets/HeroSection.jsx index 905d404..ad496fb 100644 --- a/Topers.Client/src/widgets/HeroSection.jsx +++ b/Topers.Client/src/widgets/HeroSection.jsx @@ -1,6 +1,6 @@ import React from "react"; import { Box, Flex, Text, Heading, Button, Image } from "@chakra-ui/react"; -import { FaLongArrowAltRight } from "react-icons/fa"; +import { ArrowForwardIcon } from "@chakra-ui/icons"; import handsImage from "../app/images/stock_bg_hands.jpg"; export default function HeroSection() { @@ -24,18 +24,18 @@ export default function HeroSection() {
- + Hands Date: Fri, 19 Jul 2024 12:08:14 +0300 Subject: [PATCH 23/39] Add CORS settings --- Topers.Api/Program.cs | 3 +++ Topers.Api/Topers.Api.csproj | 1 + 2 files changed, 4 insertions(+) diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 38d5f86..8a3031d 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -114,6 +114,9 @@ HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always }); + app.UseCors(options => options.WithOrigins("http://localhost:5173/") + .AllowAnyHeader() + .AllowAnyMethod()); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/Topers.Api/Topers.Api.csproj b/Topers.Api/Topers.Api.csproj index 960dcb3..64030b2 100644 --- a/Topers.Api/Topers.Api.csproj +++ b/Topers.Api/Topers.Api.csproj @@ -16,6 +16,7 @@ + From d2787290a93972f712880dc1e3319453e30ac99b Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 19 Jul 2024 22:42:30 +0300 Subject: [PATCH 24/39] Add auth drawer. Set up a good section --- Topers.Api/Program.cs | 2 +- Topers.Client/index.html | 2 +- Topers.Client/package-lock.json | 15 ++++ Topers.Client/package.json | 1 + Topers.Client/src/entities/Good.ts | 4 +- Topers.Client/src/entities/Scope.ts | 7 ++ Topers.Client/src/{main.jsx => main.tsx} | 2 +- .../src/pages/{MainPage.jsx => MainPage.tsx} | 4 +- Topers.Client/src/shared/GoodsSection.tsx | 52 ++++++++++++ .../HeroSection.tsx} | 0 Topers.Client/src/widgets/AuthDrawer.tsx | 81 +++++++++++++++++++ Topers.Client/src/widgets/GoodCard.jsx | 47 ----------- Topers.Client/src/widgets/GoodCard.tsx | 68 ++++++++++++++++ Topers.Client/src/widgets/GoodsSection.jsx | 29 ------- .../src/widgets/{Header.jsx => Header.tsx} | 12 +-- Topers.Client/tsconfig.json | 19 +++++ 16 files changed, 257 insertions(+), 88 deletions(-) create mode 100644 Topers.Client/src/entities/Scope.ts rename Topers.Client/src/{main.jsx => main.tsx} (75%) rename Topers.Client/src/pages/{MainPage.jsx => MainPage.tsx} (85%) create mode 100644 Topers.Client/src/shared/GoodsSection.tsx rename Topers.Client/src/{widgets/HeroSection.jsx => shared/HeroSection.tsx} (100%) create mode 100644 Topers.Client/src/widgets/AuthDrawer.tsx delete mode 100644 Topers.Client/src/widgets/GoodCard.jsx create mode 100644 Topers.Client/src/widgets/GoodCard.tsx delete mode 100644 Topers.Client/src/widgets/GoodsSection.jsx rename Topers.Client/src/widgets/{Header.jsx => Header.tsx} (92%) create mode 100644 Topers.Client/tsconfig.json diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 8a3031d..03c9093 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -114,7 +114,7 @@ HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always }); - app.UseCors(options => options.WithOrigins("http://localhost:5173/") + app.UseCors(options => options.WithOrigins("http://localhost:5173") .AllowAnyHeader() .AllowAnyMethod()); app.UseAuthentication(); diff --git a/Topers.Client/index.html b/Topers.Client/index.html index bcaafb3..86dc266 100644 --- a/Topers.Client/index.html +++ b/Topers.Client/index.html @@ -8,6 +8,6 @@
- + diff --git a/Topers.Client/package-lock.json b/Topers.Client/package-lock.json index 993dc94..86097a0 100644 --- a/Topers.Client/package-lock.json +++ b/Topers.Client/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-react": "^7.34.2", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.5.3", "vite": "^5.3.1" } }, @@ -6321,6 +6322,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/Topers.Client/package.json b/Topers.Client/package.json index 757a8ef..6533ece 100644 --- a/Topers.Client/package.json +++ b/Topers.Client/package.json @@ -28,6 +28,7 @@ "eslint-plugin-react": "^7.34.2", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.5.3", "vite": "^5.3.1" } } diff --git a/Topers.Client/src/entities/Good.ts b/Topers.Client/src/entities/Good.ts index fdf49f6..a4d85e4 100644 --- a/Topers.Client/src/entities/Good.ts +++ b/Topers.Client/src/entities/Good.ts @@ -1,6 +1,8 @@ +import { Scope } from "./Scope"; + export interface Good { id: string; name: string; description: string; - image: string; + scopes: Scope[] } \ No newline at end of file diff --git a/Topers.Client/src/entities/Scope.ts b/Topers.Client/src/entities/Scope.ts new file mode 100644 index 0000000..68d0856 --- /dev/null +++ b/Topers.Client/src/entities/Scope.ts @@ -0,0 +1,7 @@ +export interface Scope { + id: string; + goodId: string, + litre: number; + price: number; + imageName: string; +} \ No newline at end of file diff --git a/Topers.Client/src/main.jsx b/Topers.Client/src/main.tsx similarity index 75% rename from Topers.Client/src/main.jsx rename to Topers.Client/src/main.tsx index dd29994..5233105 100644 --- a/Topers.Client/src/main.jsx +++ b/Topers.Client/src/main.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client' import App from './pages/MainPage' import './app/styles/index.css' -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById('root')!).render( , diff --git a/Topers.Client/src/pages/MainPage.jsx b/Topers.Client/src/pages/MainPage.tsx similarity index 85% rename from Topers.Client/src/pages/MainPage.jsx rename to Topers.Client/src/pages/MainPage.tsx index 3e3c4f7..e3ff92c 100644 --- a/Topers.Client/src/pages/MainPage.jsx +++ b/Topers.Client/src/pages/MainPage.tsx @@ -1,7 +1,7 @@ import Header from '../widgets/Header' import { ChakraProvider, Box, Flex, Heading, Text, Button, Image } from '@chakra-ui/react' -import HeroSection from '../widgets/HeroSection' -import GoodsSection from '../widgets/GoodsSection' +import HeroSection from '../shared/HeroSection' +import GoodsSection from '../shared/GoodsSection' function MainPage() { diff --git a/Topers.Client/src/shared/GoodsSection.tsx b/Topers.Client/src/shared/GoodsSection.tsx new file mode 100644 index 0000000..687ca07 --- /dev/null +++ b/Topers.Client/src/shared/GoodsSection.tsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import GoodCard from '../widgets/GoodCard'; +import { Box, Flex, Heading } from '@chakra-ui/react'; +import { Good } from '../entities/Good'; + +export default function GoodsSection() { + const [goods, setGoods] = useState([]); + const baseUrl = 'http://localhost:5264'; + + useEffect(() => { + axios.get(`${baseUrl}/api/goods`) + .then(response => { + setGoods(response.data); + console.log(response.data); + }) + .catch(error => { + console.error("There are some problems: ", error); + }); + }, []); + + return ( + + Our goods + + {goods.map(good => ( + + {good.name} + {good.scopes.map(scope => ( + + + + ))} + + ))} + + + ); +} diff --git a/Topers.Client/src/widgets/HeroSection.jsx b/Topers.Client/src/shared/HeroSection.tsx similarity index 100% rename from Topers.Client/src/widgets/HeroSection.jsx rename to Topers.Client/src/shared/HeroSection.tsx diff --git a/Topers.Client/src/widgets/AuthDrawer.tsx b/Topers.Client/src/widgets/AuthDrawer.tsx new file mode 100644 index 0000000..21c762d --- /dev/null +++ b/Topers.Client/src/widgets/AuthDrawer.tsx @@ -0,0 +1,81 @@ +import { AddIcon } from "@chakra-ui/icons"; +import { + Box, + Button, + Drawer, + DrawerBody, + DrawerCloseButton, + DrawerContent, + DrawerFooter, + DrawerHeader, + DrawerOverlay, + Flex, + FormLabel, + Input, + InputGroup, + InputLeftAddon, + InputRightAddon, + Select, + Stack, + Textarea, + useDisclosure, +} from "@chakra-ui/react"; +import React from "react"; + +interface AuthDrawerProps { + isOpen: boolean; + onClose: () => void; +} + +const AuthDrawer: React.FC = ({ isOpen, onClose }) => { + const firstField = React.useRef(null); + + return ( + + + + + + Create a new account + + + + + + Username + + + + + Password + + + + + + + + + + + + + ); +}; + +export default AuthDrawer; \ No newline at end of file diff --git a/Topers.Client/src/widgets/GoodCard.jsx b/Topers.Client/src/widgets/GoodCard.jsx deleted file mode 100644 index da30e53..0000000 --- a/Topers.Client/src/widgets/GoodCard.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Card, - CardBody, - Image, - Text, - Heading, - Stack, - ButtonGroup, - Button, - CardFooter, - Divider -} from "@chakra-ui/react"; -import React from "react"; - -export default function GoodCard({ goodTitle, goodDescription, goodImage, goodPrice }) { - return ( - - - Green double couch with wooden legs - - {goodTitle} - - {goodDescription} - - - {goodPrice} $ - - - - - - - - - - - - ); -} diff --git a/Topers.Client/src/widgets/GoodCard.tsx b/Topers.Client/src/widgets/GoodCard.tsx new file mode 100644 index 0000000..e22cee8 --- /dev/null +++ b/Topers.Client/src/widgets/GoodCard.tsx @@ -0,0 +1,68 @@ +import { + Card, + CardBody, + Image, + Text, + Heading, + Stack, + ButtonGroup, + Button, + CardFooter, + Divider, +} from "@chakra-ui/react"; +import React from "react"; + +interface GoodCardProps { + goodId: string; + goodTitle: string; + goodDescription: string; + goodLitre: number; + goodImage: string; + goodPrice: number; +} + +const GoodCard: React.FC = ({ + goodId, + goodTitle, + goodDescription, + goodLitre, + goodImage, + goodPrice +}) => { + return ( + + + {goodTitle} + + {goodLitre} litre + + {goodDescription} + + + {goodPrice} $ + + + + + + + + + + + + ); +}; + +export default GoodCard; \ No newline at end of file diff --git a/Topers.Client/src/widgets/GoodsSection.jsx b/Topers.Client/src/widgets/GoodsSection.jsx deleted file mode 100644 index 13bf915..0000000 --- a/Topers.Client/src/widgets/GoodsSection.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useEffect, useState } from 'react' -import axios from 'axios'; -import GoodCard from './GoodCard' -import { Box } from '@chakra-ui/react'; - -export default function GoodsSection() { - const [goods, setGoods] = useState([]); - const baseUrl = 'http://localhost:5264'; - - useEffect(() => { - axios.get("http://localhost:5264/api/goods") - .then(response => { - setGoods(response.data); - }) - .catch(error => { - console.error("There are some problems: ", error); - }) - , []}) - - return ( - - {goods.map(good => ( - - ))} - - ) -} diff --git a/Topers.Client/src/widgets/Header.jsx b/Topers.Client/src/widgets/Header.tsx similarity index 92% rename from Topers.Client/src/widgets/Header.jsx rename to Topers.Client/src/widgets/Header.tsx index e8b1134..d9cee08 100644 --- a/Topers.Client/src/widgets/Header.jsx +++ b/Topers.Client/src/widgets/Header.tsx @@ -1,16 +1,15 @@ import React, { useState } from 'react'; -import { Image, Box, Divider, Button, Flex, Link, IconButton, Collapse, VStack } from '@chakra-ui/react'; +import { Image, Box, Divider, Button, Flex, Link, IconButton, Collapse, VStack, useDisclosure } from '@chakra-ui/react'; import Logo from '../app/images/logo.svg'; import Cart from '../shared/Cart'; import { HamburgerIcon } from '@chakra-ui/icons'; +import AuthDrawer from './AuthDrawer'; export default function Header() { const [cartOpen, setCartOpen] = useState(false); const [cartItemCount, setCartItemCount] = useState(1); const [cartTotalPrice, setCartTotalPrice] = useState(100); - const [isOpen, setIsOpen] = useState(false); - - const toggleMenu = () => setIsOpen(!isOpen); + const { isOpen, onOpen, onClose } = useDisclosure(); return (
@@ -36,9 +35,11 @@ export default function Header() { colorScheme='transparent' color='black' size='sm' - border='1px solid gray'> + border='1px solid gray' + onClick={onOpen}> Sign In + } - onClick={toggleMenu} display={{ base: 'block', md: 'none' }} /> diff --git a/Topers.Client/tsconfig.json b/Topers.Client/tsconfig.json new file mode 100644 index 0000000..9c10f12 --- /dev/null +++ b/Topers.Client/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["node_modules", "build"] +} From 9abad52703c3066e33aa99331bc8a413cf3e6522 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 21 Jul 2024 18:53:29 +0300 Subject: [PATCH 25/39] Update sections. Add Good features, modal and card settings --- Topers.Client/src/features/Goods.ts | 44 ++++++++++++++++ Topers.Client/src/pages/MainPage.tsx | 7 +-- Topers.Client/src/shared/GoodsSection.tsx | 61 +++++++++++------------ Topers.Client/src/shared/HeroSection.tsx | 9 +++- Topers.Client/src/widgets/GoodCard.tsx | 12 ++--- Topers.Client/src/widgets/GoodModal.tsx | 49 ++++++++++++++++++ Topers.Client/src/widgets/Header.tsx | 2 +- 7 files changed, 141 insertions(+), 43 deletions(-) create mode 100644 Topers.Client/src/features/Goods.ts create mode 100644 Topers.Client/src/widgets/GoodModal.tsx diff --git a/Topers.Client/src/features/Goods.ts b/Topers.Client/src/features/Goods.ts new file mode 100644 index 0000000..3fa749b --- /dev/null +++ b/Topers.Client/src/features/Goods.ts @@ -0,0 +1,44 @@ +import axios from "axios"; +import { Good } from "../entities/Good"; + +const baseUrl = 'http://localhost:5264'; + +export const GetAllGoods = async () => { + try { + const response = await axios.get(`${baseUrl}/api/goods`) + return response.data; + } catch (error) { + console.error("Error in receiving goods: ", error); + throw error; + } +}; + +export const GetGoodById = async (id: string) => { + try { + const response = await axios.get(`${baseUrl}/api/goods/${id}`) + return response.data; + } catch (error) { + console.error("Error in receiving good: ", error); + throw error; + } +}; + +export const UpdateGoodById = async (id: string, data: Good) => { + try { + const response = await axios.put(`${baseUrl}/api/goods/${id}`, data) + return response.data; + } catch (error) { + console.error("Error in updating good: ", error); + throw error; + } +}; + +export const DeleteGoodsById = async (id: string) => { + try { + const response = await axios.delete(`${baseUrl}/api/goods/${id}`) + return response.data; + } catch (error) { + console.error("Error in deleting good: ", error); + throw error; + } +}; \ No newline at end of file diff --git a/Topers.Client/src/pages/MainPage.tsx b/Topers.Client/src/pages/MainPage.tsx index e3ff92c..b8b4a8b 100644 --- a/Topers.Client/src/pages/MainPage.tsx +++ b/Topers.Client/src/pages/MainPage.tsx @@ -1,5 +1,5 @@ import Header from '../widgets/Header' -import { ChakraProvider, Box, Flex, Heading, Text, Button, Image } from '@chakra-ui/react' +import { ChakraProvider, Box } from '@chakra-ui/react' import HeroSection from '../shared/HeroSection' import GoodsSection from '../shared/GoodsSection' @@ -11,14 +11,15 @@ function MainPage() { -
+
+ margin='auto' + pt='20px'> diff --git a/Topers.Client/src/shared/GoodsSection.tsx b/Topers.Client/src/shared/GoodsSection.tsx index 687ca07..eb28a71 100644 --- a/Topers.Client/src/shared/GoodsSection.tsx +++ b/Topers.Client/src/shared/GoodsSection.tsx @@ -1,52 +1,51 @@ import React, { useEffect, useState } from 'react'; -import axios from 'axios'; -import GoodCard from '../widgets/GoodCard'; -import { Box, Flex, Heading } from '@chakra-ui/react'; +import { GoodCard } from '../widgets/GoodCard'; +import { Box, Flex, Grid, Heading } from '@chakra-ui/react'; import { Good } from '../entities/Good'; +import { GetAllGoods } from '../features/Goods'; export default function GoodsSection() { const [goods, setGoods] = useState([]); const baseUrl = 'http://localhost:5264'; useEffect(() => { - axios.get(`${baseUrl}/api/goods`) - .then(response => { - setGoods(response.data); - console.log(response.data); - }) - .catch(error => { - console.error("There are some problems: ", error); - }); + const fetchGoods = async() => { + const goodsData = await GetAllGoods(); + setGoods(goodsData); + } + fetchGoods(); }, []); return ( - + Our goods - - {goods.map(good => ( + + {goods.map(good => ( - {good.name} - {good.scopes.map(scope => ( - - - - ))} + + + {good.scopes.map(scope => ( + + + + ))} + + ))} - + ); } diff --git a/Topers.Client/src/shared/HeroSection.tsx b/Topers.Client/src/shared/HeroSection.tsx index ad496fb..a20430a 100644 --- a/Topers.Client/src/shared/HeroSection.tsx +++ b/Topers.Client/src/shared/HeroSection.tsx @@ -29,7 +29,14 @@ export default function HeroSection() { Goods - + Hands = ({ +export const GoodCard = ({ goodId, goodTitle, goodDescription, goodLitre, goodImage, goodPrice -}) => { +}: GoodCardProps) => { return ( @@ -41,11 +41,11 @@ const GoodCard: React.FC = ({ mx="auto" /> - {goodLitre} litre + {goodTitle} ({goodLitre} litre) {goodDescription} - + {goodPrice} $ @@ -63,6 +63,4 @@ const GoodCard: React.FC = ({ ); -}; - -export default GoodCard; \ No newline at end of file +}; \ No newline at end of file diff --git a/Topers.Client/src/widgets/GoodModal.tsx b/Topers.Client/src/widgets/GoodModal.tsx new file mode 100644 index 0000000..81c9ec8 --- /dev/null +++ b/Topers.Client/src/widgets/GoodModal.tsx @@ -0,0 +1,49 @@ +import { Button, FormControl, FormLabel, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, useDisclosure } from "@chakra-ui/react" +import React from "react" + +function InitialFocus() { + const { isOpen, onOpen, onClose } = useDisclosure() + + const initialRef = React.useRef(null) + const finalRef = React.useRef(null) + + return ( + <> + + + + + + + Create your account + + + + First name + + + + + Last name + + + + + + + + + + + + ) + } \ No newline at end of file diff --git a/Topers.Client/src/widgets/Header.tsx b/Topers.Client/src/widgets/Header.tsx index d9cee08..cee7202 100644 --- a/Topers.Client/src/widgets/Header.tsx +++ b/Topers.Client/src/widgets/Header.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Image, Box, Divider, Button, Flex, Link, IconButton, Collapse, VStack, useDisclosure } from '@chakra-ui/react'; -import Logo from '../app/images/logo.svg'; +import Logo from '../app/images/logo.svg' import Cart from '../shared/Cart'; import { HamburgerIcon } from '@chakra-ui/icons'; import AuthDrawer from './AuthDrawer'; From e549d0a4b1bfde1ffa36eb02dac3004de8137007 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 21 Jul 2024 20:20:32 +0300 Subject: [PATCH 26/39] Set up a correct Scope ref to OrderDetails. Update configurations --- .../Configurations/GoodConfiguration.cs | 4 - .../OrderDetailsConfiguration.cs | 3 +- .../Entities/GoodEntity.cs | 5 - .../Entities/GoodScopeEntity.cs | 5 + .../Entities/OrderDetailsEntity.cs | 4 +- .../20240721171553_UpdateScopeRef.Designer.cs | 318 ++++++++++++++++++ .../20240721171553_UpdateScopeRef.cs | 42 +++ .../TopersDbContextModelSnapshot.cs | 9 +- 8 files changed, 376 insertions(+), 14 deletions(-) create mode 100644 Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.cs diff --git a/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs index 3e23d8b..6eb3eb5 100644 --- a/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs +++ b/Topers.DataAccess.Postgres/Configurations/GoodConfiguration.cs @@ -17,9 +17,5 @@ public void Configure(EntityTypeBuilder builder) .HasMany(g => g.Scopes) .WithOne(s => s.Good) .HasForeignKey(s => s.GoodId); - builder - .HasMany(g => g.OrderDetails) - .WithOne(d => d.Good) - .HasForeignKey(d => d.GoodId); } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs index 86f3e07..5d1f5a3 100644 --- a/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs +++ b/Topers.DataAccess.Postgres/Configurations/OrderDetailsConfiguration.cs @@ -15,7 +15,8 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(d => d.OrderId); builder .HasOne(d => d.Good) - .WithMany(g => g.OrderDetails) + .WithMany(o => o.OrderDetails) .HasForeignKey(d => d.GoodId); + } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs index 1b8482e..cb1abfe 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodEntity.cs @@ -34,9 +34,4 @@ public class GoodEntity /// Gets or sets a good scopes collection. ///
public ICollection Scopes { get; set; } = []; - - /// - /// Gets or sets an order details about good. - /// - public ICollection? OrderDetails { get; set; } = []; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs index e555853..3f45a5d 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs @@ -43,4 +43,9 @@ public class GoodScopeEntity /// Gets or sets a good image file. ///
public IFormFile? ImageFile { get; set; } + + /// + /// Gets or sets an order details about good. + /// + public ICollection? OrderDetails { get; set; } = []; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs b/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs index 65a2723..e8dc2fa 100644 --- a/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/OrderDetailsEntity.cs @@ -1,3 +1,5 @@ +using Topers.Core.Models; + namespace Topers.DataAccess.Postgres.Entities; /// @@ -28,7 +30,7 @@ public class OrderDetailsEntity /// /// Gets or sets a good. /// - public GoodEntity Good { get; set; } = null!; + public GoodScopeEntity Good { get; set; } = null!; /// /// Gets or sets a good quantity. diff --git a/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.Designer.cs new file mode 100644 index 0000000..733e0a9 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.Designer.cs @@ -0,0 +1,318 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240721171553_UpdateScopeRef")] + partial class UpdateScopeRef + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Address"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Navigation("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.cs b/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.cs new file mode 100644 index 0000000..cdce613 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240721171553_UpdateScopeRef.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class UpdateScopeRef : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrderDetails_Goods_GoodId", + table: "OrderDetails"); + + migrationBuilder.AddForeignKey( + name: "FK_OrderDetails_GoodScopes_GoodId", + table: "OrderDetails", + column: "GoodId", + principalTable: "GoodScopes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrderDetails_GoodScopes_GoodId", + table: "OrderDetails"); + + migrationBuilder.AddForeignKey( + name: "FK_OrderDetails_Goods_GoodId", + table: "OrderDetails", + column: "GoodId", + principalTable: "Goods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs index 120938d..3881ab8 100644 --- a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -255,7 +255,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => { - b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", "Good") .WithMany("OrderDetails") .HasForeignKey("GoodId") .OnDelete(DeleteBehavior.Cascade) @@ -297,11 +297,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => { - b.Navigation("OrderDetails"); - b.Navigation("Scopes"); }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Navigation("OrderDetails"); + }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => { b.Navigation("OrderDetails"); From 5ab470ecce8b1c13c077394ccc365ae12d220245 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 22 Jul 2024 10:43:16 +0300 Subject: [PATCH 27/39] Implement IOrdersRepository & IOrdersService. Set up a Program.cs --- Topers.Api/Program.cs | 2 + Topers.Core/Abstractions/IOrdersRepository.cs | 12 +++ Topers.Core/Abstractions/IOrdersService.cs | 13 ++++ .../Repositories/OrdersRepository.cs | 77 +++++++++++++++++++ .../Services/OrdersService.cs | 55 +++++++++++++ 5 files changed, 159 insertions(+) create mode 100644 Topers.Core/Abstractions/IOrdersRepository.cs create mode 100644 Topers.Core/Abstractions/IOrdersService.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs create mode 100644 Topers.Infrastructure/Services/OrdersService.cs diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 03c9093..e3aefe3 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -66,12 +66,14 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Topers.Core/Abstractions/IOrdersRepository.cs b/Topers.Core/Abstractions/IOrdersRepository.cs new file mode 100644 index 0000000..e9f5511 --- /dev/null +++ b/Topers.Core/Abstractions/IOrdersRepository.cs @@ -0,0 +1,12 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface IOrdersRepository +{ + Task> GetAllAsync(); + Task GetByIdAsync(Guid orderId); + Task CreateAsync(Order order); + Task UpdateAsync(Order order); + Task DeleteAsync(Guid orderId); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IOrdersService.cs b/Topers.Core/Abstractions/IOrdersService.cs new file mode 100644 index 0000000..e3439bc --- /dev/null +++ b/Topers.Core/Abstractions/IOrdersService.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface IOrdersService +{ + Task> GetAllOrdersAsync(); + Task GetOrderByIdAsync(Guid orderId); + Task CreateOrderAsync(Order order); + Task UpdateOrderAsync(Order order); + Task DeleteOrderAsync(Guid orderId); +}; \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs new file mode 100644 index 0000000..674c74f --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -0,0 +1,77 @@ +using System.Runtime.CompilerServices; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class OrdersRepository : IOrdersRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public OrdersRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task CreateAsync(Order order) + { + var orderEntity = new OrderEntity + { + Id = Guid.NewGuid(), + Date = DateTime.UtcNow, + CustomerId = order.Customer.Id, + TotalPrice = order.TotalPrice + }; + + await _context.Orders.AddAsync(orderEntity); + await _context.SaveChangesAsync(); + + return orderEntity.Id; + } + + public async Task DeleteAsync(Guid orderId) + { + await _context.Orders + .Where(o => o.Id == orderId) + .ExecuteDeleteAsync(); + + return orderId; + } + + public async Task> GetAllAsync() + { + var orderEntity = await _context.Orders + .AsNoTracking() + .ToListAsync(); + + var orderEntitiesDto = _mapper.Map>(orderEntity); + + return orderEntitiesDto; + } + + public async Task GetByIdAsync(Guid orderId) + { + var orderEntity = await _context.Orders + .AsNoTracking() + .FirstOrDefaultAsync(o => o.Id == orderId); + + var orderEntityDto = _mapper.Map(orderEntity); + + return orderEntityDto; + } + + public async Task UpdateAsync(Order order) + { + await _context.Orders + .Where(o => o.Id == order.Id) + .ExecuteUpdateAsync(oUpdate => oUpdate + .SetProperty(o => o.CustomerId, order.Customer.Id)); + + return order.Id; + } +} \ No newline at end of file diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs new file mode 100644 index 0000000..0508a5e --- /dev/null +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class OrdersService : IOrdersService +{ + private readonly IOrdersRepository _ordersRepository; + private readonly IMapper _mapper; + + public OrdersService( + IOrdersRepository ordersRepository, + IMapper mapper) + { + _ordersRepository = ordersRepository; + _mapper = mapper; + } + + public async Task CreateOrderAsync(Order order) + { + var newOrderIdentifier = await _ordersRepository.CreateAsync(order); + + var newOrder = new OrderResponseDto + ( + newOrderIdentifier, + order.Date, + order.Customer.Id, + order.TotalPrice + ); + + return newOrder; + } + + public async Task DeleteOrderAsync(Guid orderId) + { + return await _ordersRepository.DeleteAsync(orderId); + } + + public async Task> GetAllOrdersAsync() + { + return _mapper.Map>(await _ordersRepository.GetAllAsync()); + } + + public async Task GetOrderByIdAsync(Guid orderId) + { + return _mapper.Map(await _ordersRepository.GetByIdAsync(orderId)); + } + + public async Task UpdateOrderAsync(Order order) + { + return await _ordersRepository.UpdateAsync(order); + } +} \ No newline at end of file From a15de475ff3113c7f70091df5076d6aa45969af8 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 22 Jul 2024 12:28:45 +0300 Subject: [PATCH 28/39] Add UpdateOrderRequestDto. Implement OrdersController --- Topers.Api/Controllers/OrdersController.cs | 83 +++++++++++++++++++ Topers.Core/Dtos/OrderDto.cs | 4 + Topers.Core/Models/Order.cs | 8 +- .../Repositories/OrdersRepository.cs | 5 +- .../Services/OrdersService.cs | 2 +- 5 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 Topers.Api/Controllers/OrdersController.cs diff --git a/Topers.Api/Controllers/OrdersController.cs b/Topers.Api/Controllers/OrdersController.cs new file mode 100644 index 0000000..03fe9d3 --- /dev/null +++ b/Topers.Api/Controllers/OrdersController.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Api.Controllers +{ + [ApiController] + [Route("api/orders")] + public class OrdersController( + IOrdersService orderService + ) : ControllerBase + { + private readonly IOrdersService _ordersService = orderService; + + [HttpGet] + [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(IEnumerable))] + [SwaggerResponse(400, Description = "Orders not found.")] + public async Task>> GetOrders() + { + var orders = await _ordersService.GetAllOrdersAsync(); + + if (orders == null) + { + return NotFound(); + } + + return Ok(orders); + } + + [HttpGet("{orderId:guid}")] + [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(OrderResponseDto))] + [SwaggerResponse(400, Description = "Orders not found.")] + public async Task> GetOrderById([FromRoute] Guid orderId) + { + var order = await _ordersService.GetOrderByIdAsync(orderId); + + if (order == null) + { + return NotFound(); + } + + return Ok(order); + } + + [HttpPost("create")] + [SwaggerResponse(200, Description = "Create a new order.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] + public async Task> CreateGood([FromBody] OrderRequestDto order) + { + var newOrder = new Order + ( + Guid.Empty, + order.Date, + order.CustomerId, + 0 + ); + + var newOrderEntity = await _ordersService.CreateOrderAsync(newOrder); + + return Ok(newOrderEntity); + } + + [HttpPut("{orderId:guid}")] + [SwaggerResponse(200, Description = "Update an existing order.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] + public async Task> UpdateOrder([FromRoute] Guid orderId, [FromBody] UpdateOrderRequestDto order) + { + var existOrder = new Order + ( + orderId, + DateTime.UtcNow, + order.CustomerId, + Decimal.MinValue + ); + + var updatedOrder = await _ordersService.UpdateOrderAsync(existOrder); + + return Ok(updatedOrder); + } + } +} \ No newline at end of file diff --git a/Topers.Core/Dtos/OrderDto.cs b/Topers.Core/Dtos/OrderDto.cs index 02e2c07..0f847de 100644 --- a/Topers.Core/Dtos/OrderDto.cs +++ b/Topers.Core/Dtos/OrderDto.cs @@ -11,4 +11,8 @@ public record OrderRequestDto( DateTime Date, Guid CustomerId, decimal TotalPrice +); + +public record UpdateOrderRequestDto( + Guid CustomerId ); \ No newline at end of file diff --git a/Topers.Core/Models/Order.cs b/Topers.Core/Models/Order.cs index 6adf581..252598f 100644 --- a/Topers.Core/Models/Order.cs +++ b/Topers.Core/Models/Order.cs @@ -5,11 +5,11 @@ namespace Topers.Core.Models; /// public class Order { - public Order(Guid id, DateTime date, Customer customer, decimal totalPrice) + public Order(Guid id, DateTime date, Guid customerId, decimal totalPrice) { Id = id; Date = date; - Customer = customer; + CustomerId = customerId; TotalPrice = totalPrice; } @@ -24,9 +24,9 @@ public Order(Guid id, DateTime date, Customer customer, decimal totalPrice) public DateTime Date { get; } /// - /// Gets or sets an order customer. + /// Gets or sets an order customer identifier. /// - public Customer Customer { get; } = null!; + public Guid CustomerId { get; } /// /// Gets or sets an order total price. diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs index 674c74f..39abc3b 100644 --- a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using AutoMapper; using Microsoft.EntityFrameworkCore; using Topers.Core.Abstractions; @@ -24,7 +23,7 @@ public async Task CreateAsync(Order order) { Id = Guid.NewGuid(), Date = DateTime.UtcNow, - CustomerId = order.Customer.Id, + CustomerId = order.CustomerId, TotalPrice = order.TotalPrice }; @@ -70,7 +69,7 @@ public async Task UpdateAsync(Order order) await _context.Orders .Where(o => o.Id == order.Id) .ExecuteUpdateAsync(oUpdate => oUpdate - .SetProperty(o => o.CustomerId, order.Customer.Id)); + .SetProperty(o => o.CustomerId, order.CustomerId)); return order.Id; } diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs index 0508a5e..aa65a15 100644 --- a/Topers.Infrastructure/Services/OrdersService.cs +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -26,7 +26,7 @@ public async Task CreateOrderAsync(Order order) ( newOrderIdentifier, order.Date, - order.Customer.Id, + order.CustomerId, order.TotalPrice ); From 5a83921d880c59c2463f61c30b561f0a6c6b7530 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 22 Jul 2024 13:38:20 +0300 Subject: [PATCH 29/39] Add the ability to add a good to an order --- Topers.Api/Controllers/OrdersController.cs | 24 +++++++++++++++++++ Topers.Core/Abstractions/IOrdersRepository.cs | 1 + Topers.Core/Abstractions/IOrdersService.cs | 1 + Topers.Core/Dtos/OrderDto.cs | 6 +++++ Topers.Core/Models/GoodScope.cs | 2 +- Topers.Core/Models/OrderDetails.cs | 5 ++-- .../Repositories/OrdersRepository.cs | 17 +++++++++++++ .../Services/OrdersService.cs | 18 ++++++++++++++ 8 files changed, 70 insertions(+), 4 deletions(-) diff --git a/Topers.Api/Controllers/OrdersController.cs b/Topers.Api/Controllers/OrdersController.cs index 03fe9d3..837fac0 100644 --- a/Topers.Api/Controllers/OrdersController.cs +++ b/Topers.Api/Controllers/OrdersController.cs @@ -79,5 +79,29 @@ public async Task> UpdateOrder([FromRoute] Guid o return Ok(updatedOrder); } + + [HttpPost("{orderId:guid}/addGood")] + [SwaggerResponse(200, Description = "Add good to an existing order.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] + public async Task> AddProductToOrder([FromRoute] Guid orderId, [FromBody] AddProductToOrderRequestDto orderDetail) + { + var newGoodDetail = new OrderDetails + ( + orderId, + orderDetail.GoodScopeId, + orderDetail.GoodQuantity + ); + + var newGoodScope = new GoodScope + ( + Guid.Empty, + orderDetail.GoodScopeId, + orderDetail.GoodLitre + ); + + var newGoodDetailIdentifier = await _ordersService.AddGoodToOrderAsync(newGoodDetail, newGoodScope); + + return Ok(newGoodDetailIdentifier); + } } } \ No newline at end of file diff --git a/Topers.Core/Abstractions/IOrdersRepository.cs b/Topers.Core/Abstractions/IOrdersRepository.cs index e9f5511..9634604 100644 --- a/Topers.Core/Abstractions/IOrdersRepository.cs +++ b/Topers.Core/Abstractions/IOrdersRepository.cs @@ -9,4 +9,5 @@ public interface IOrdersRepository Task CreateAsync(Order order); Task UpdateAsync(Order order); Task DeleteAsync(Guid orderId); + Task AddDetailAsync(OrderDetails detail); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IOrdersService.cs b/Topers.Core/Abstractions/IOrdersService.cs index e3439bc..65613f0 100644 --- a/Topers.Core/Abstractions/IOrdersService.cs +++ b/Topers.Core/Abstractions/IOrdersService.cs @@ -10,4 +10,5 @@ public interface IOrdersService Task CreateOrderAsync(Order order); Task UpdateOrderAsync(Order order); Task DeleteOrderAsync(Guid orderId); + Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good); }; \ No newline at end of file diff --git a/Topers.Core/Dtos/OrderDto.cs b/Topers.Core/Dtos/OrderDto.cs index 0f847de..6d46333 100644 --- a/Topers.Core/Dtos/OrderDto.cs +++ b/Topers.Core/Dtos/OrderDto.cs @@ -15,4 +15,10 @@ decimal TotalPrice public record UpdateOrderRequestDto( Guid CustomerId +); + +public record AddProductToOrderRequestDto( + Guid GoodScopeId, + int GoodQuantity, + int GoodLitre ); \ No newline at end of file diff --git a/Topers.Core/Models/GoodScope.cs b/Topers.Core/Models/GoodScope.cs index 15a14b6..976d661 100644 --- a/Topers.Core/Models/GoodScope.cs +++ b/Topers.Core/Models/GoodScope.cs @@ -5,7 +5,7 @@ namespace Topers.Core.Models; /// public class GoodScope { - public GoodScope(Guid id, Guid goodId, int litre, decimal price, string? image) + public GoodScope(Guid id, Guid goodId, int litre, decimal price = 0, string? image = null) { Id = id; GoodId = goodId; diff --git a/Topers.Core/Models/OrderDetails.cs b/Topers.Core/Models/OrderDetails.cs index 501a970..cdf9ff6 100644 --- a/Topers.Core/Models/OrderDetails.cs +++ b/Topers.Core/Models/OrderDetails.cs @@ -5,13 +5,12 @@ namespace Topers.Core.Models; /// public class OrderDetails { - public OrderDetails(Guid id, Guid orderId, Guid goodId, int quantity, decimal price) + public OrderDetails(Guid orderId, Guid goodId, int quantity, decimal totalPrice = 0) { - Id = id; OrderId = orderId; GoodId = goodId; Quantity = quantity; - Price = price; + Price = totalPrice; } /// diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs index 39abc3b..c5f8ad6 100644 --- a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -73,4 +73,21 @@ await _context.Orders return order.Id; } + + public async Task AddDetailAsync(OrderDetails detail) + { + var orderDetailEntity = new OrderDetailsEntity + { + Id = Guid.NewGuid(), + OrderId = detail.OrderId, + GoodId = detail.GoodId, + Quantity = detail.Quantity, + Price = detail.Price + }; + + await _context.OrderDetails.AddAsync(orderDetailEntity); + await _context.SaveChangesAsync(); + + return orderDetailEntity.Id; + } } \ No newline at end of file diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs index aa65a15..1383d16 100644 --- a/Topers.Infrastructure/Services/OrdersService.cs +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -8,13 +8,16 @@ namespace Topers.Infrastructure.Services; public class OrdersService : IOrdersService { private readonly IOrdersRepository _ordersRepository; + private readonly IGoodsRepository _goodsRepository; private readonly IMapper _mapper; public OrdersService( IOrdersRepository ordersRepository, + IGoodsRepository goodsRepository, IMapper mapper) { _ordersRepository = ordersRepository; + _goodsRepository = goodsRepository; _mapper = mapper; } @@ -52,4 +55,19 @@ public async Task UpdateOrderAsync(Order order) { return await _ordersRepository.UpdateAsync(order); } + + public async Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good) + { + var goodScope = await _goodsRepository.GetScopeAsync(detail.GoodId, good.Litre); + + var newGoodDetailEntity = new OrderDetails + ( + detail.OrderId, + detail.GoodId, + detail.Quantity, + goodScope.Price + ); + + return await _ordersRepository.AddDetailAsync(newGoodDetailEntity); + } } \ No newline at end of file From 63b0d581ce728a520c64ae09b72d11c28ebcb2b9 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 22 Jul 2024 20:51:07 +0300 Subject: [PATCH 30/39] Set up an order mapping profile. Update an order repository & service --- Topers.Api/Mapping/MappingProfile.cs | 29 ++++++++++++++++++- Topers.Core/Dtos/OrderDto.cs | 3 +- Topers.Core/Models/Order.cs | 6 ++++ .../Entities/OrderEntity.cs | 2 +- .../Repositories/OrdersRepository.cs | 16 +++++----- .../Services/OrdersService.cs | 2 +- 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index ab06129..8a65428 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -25,6 +25,30 @@ public MappingProfile() src.Address.Country ) : null)); + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.GoodId, opt => opt.MapFrom(src => src.GoodId)) + .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) + .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); + +CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) + .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); // Ensure this line is correctly configured + +CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) + .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); + +CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)); + + CreateMap(); CreateMap(); @@ -55,7 +79,7 @@ public MappingProfile() scope.ImageName )).ToList() : new List())); - + CreateMap(); CreateMap(); @@ -66,5 +90,8 @@ public MappingProfile() .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address)); CreateMap(); + + CreateMap(); + CreateMap(); } } \ No newline at end of file diff --git a/Topers.Core/Dtos/OrderDto.cs b/Topers.Core/Dtos/OrderDto.cs index 6d46333..65f90e8 100644 --- a/Topers.Core/Dtos/OrderDto.cs +++ b/Topers.Core/Dtos/OrderDto.cs @@ -4,7 +4,8 @@ public record OrderResponseDto( Guid Id, DateTime Date, Guid CustomerId, - decimal TotalPrice + decimal TotalPrice, + List? OrderDetails = null ); public record OrderRequestDto( diff --git a/Topers.Core/Models/Order.cs b/Topers.Core/Models/Order.cs index 252598f..047534d 100644 --- a/Topers.Core/Models/Order.cs +++ b/Topers.Core/Models/Order.cs @@ -11,6 +11,7 @@ public Order(Guid id, DateTime date, Guid customerId, decimal totalPrice) Date = date; CustomerId = customerId; TotalPrice = totalPrice; + OrderDetails = []; } /// @@ -32,4 +33,9 @@ public Order(Guid id, DateTime date, Guid customerId, decimal totalPrice) /// Gets or sets an order total price. /// public decimal TotalPrice { get; } = 0; + + /// + /// Gets or sets a good scopes. + /// + public ICollection OrderDetails { get; set; } } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/OrderEntity.cs b/Topers.DataAccess.Postgres/Entities/OrderEntity.cs index bd7f428..2ede893 100644 --- a/Topers.DataAccess.Postgres/Entities/OrderEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/OrderEntity.cs @@ -33,5 +33,5 @@ public class OrderEntity /// /// Gets or sets an order details. /// - public ICollection? OrderDetails { get; set; } = []; + public ICollection OrderDetails { get; set; } = []; } \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs index c5f8ad6..79853c5 100644 --- a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -44,18 +44,20 @@ await _context.Orders public async Task> GetAllAsync() { - var orderEntity = await _context.Orders - .AsNoTracking() - .ToListAsync(); - - var orderEntitiesDto = _mapper.Map>(orderEntity); - - return orderEntitiesDto; + var orderEntities = await _context.Orders + .Include(o => o.OrderDetails) + .AsNoTracking() + .ToListAsync(); + + var orders = _mapper.Map>(orderEntities); + + return orders; } public async Task GetByIdAsync(Guid orderId) { var orderEntity = await _context.Orders + .Include(o => o.OrderDetails) .AsNoTracking() .FirstOrDefaultAsync(o => o.Id == orderId); diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs index 1383d16..d22fcb2 100644 --- a/Topers.Infrastructure/Services/OrdersService.cs +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -63,7 +63,7 @@ public async Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good) var newGoodDetailEntity = new OrderDetails ( detail.OrderId, - detail.GoodId, + goodScope.Id, detail.Quantity, goodScope.Price ); From cd161a5e0f5e73ed8968c7ed832e2a93b3fd26a9 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Mon, 22 Jul 2024 21:28:55 +0300 Subject: [PATCH 31/39] Fix syntax bugs --- Topers.Api/Mapping/MappingProfile.cs | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index 8a65428..bcbeec2 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -31,22 +31,22 @@ public MappingProfile() .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); -CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) - .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) - .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) - .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) - .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); // Ensure this line is correctly configured - -CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) - .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) - .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) - .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) - .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); - -CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)); + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) + .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) + .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)); CreateMap(); From 4bcc01e0bf2a6f3644d8a793b31dd738405b238d Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Fri, 26 Jul 2024 20:38:53 +0300 Subject: [PATCH 32/39] Implement CancellationToken. Fix all endpoints. --- Topers.Api/Controllers/AccountController.cs | 12 ++++--- Topers.Api/Controllers/AddressesController.cs | 7 ++-- .../Controllers/CategoriesController.cs | 35 ++++++++++++------- Topers.Api/Controllers/CustomersController.cs | 16 +++++---- Topers.Api/Controllers/GoodsController.cs | 28 ++++++++++----- Topers.Api/Controllers/OrdersController.cs | 30 ++++++++++------ .../TaskCancellationHandlingMiddleware.cs | 25 +++++++++++++ Topers.Api/Program.cs | 5 +++ .../Abstractions/IAddressesRepository.cs | 2 +- Topers.Core/Abstractions/IAddressesService.cs | 2 +- .../Abstractions/ICategoriesRespository.cs | 12 +++---- .../Abstractions/ICategoriesService.cs | 12 +++---- .../Abstractions/ICustomersRepository.cs | 6 ++-- Topers.Core/Abstractions/ICustomersService.cs | 6 ++-- Topers.Core/Abstractions/IGoodsRepository.cs | 18 +++++----- Topers.Core/Abstractions/IGoodsService.cs | 16 ++++----- Topers.Core/Abstractions/IOrdersRepository.cs | 12 +++---- Topers.Core/Abstractions/IOrdersService.cs | 12 +++---- Topers.Core/Abstractions/IUsersRepository.cs | 4 +-- Topers.Core/Abstractions/IUsersService.cs | 4 +-- .../Repositories/AddressesRepository.cs | 2 +- .../Repositories/CategoriesRepository.cs | 12 +++---- .../Repositories/CustomersRepository.cs | 6 ++-- .../Repositories/GoodsRepository.cs | 18 +++++----- .../Repositories/OrdersRepository.cs | 12 +++---- .../Repositories/UsersRepository.cs | 4 +-- .../Services/AddressesService.cs | 2 +- .../Services/CategoriesService.cs | 12 +++---- .../Services/CustomersService.cs | 6 ++-- .../Services/GoodsService.cs | 16 ++++----- .../Services/OrdersService.cs | 12 +++---- .../Services/UsersService.cs | 4 +-- 32 files changed, 221 insertions(+), 149 deletions(-) create mode 100644 Topers.Api/Middleware/TaskCancellationHandlingMiddleware.cs diff --git a/Topers.Api/Controllers/AccountController.cs b/Topers.Api/Controllers/AccountController.cs index a06072c..05f48a4 100644 --- a/Topers.Api/Controllers/AccountController.cs +++ b/Topers.Api/Controllers/AccountController.cs @@ -17,17 +17,21 @@ public class AccountController( private readonly CookiesOptions _cookieOptions = options.Value; [HttpPost("register")] - public async Task Register([FromBody] RegisterUserRequestDto request) + public async Task Register( + [FromBody] RegisterUserRequestDto request, + CancellationToken cancellationToken) { - await _userService.Register(request.Username, request.Email, request.Password); + await _userService.Register(request.Username, request.Email, request.Password, cancellationToken); return Results.Ok(); } [HttpPost("login")] - public async Task Login([FromBody] LoginUserRequestDto request) + public async Task Login( + [FromBody] LoginUserRequestDto request, + CancellationToken cancellationToken) { - var token = await _userService.Login(request.Username, request.Password); + var token = await _userService.Login(request.Username, request.Password, cancellationToken); HttpContext.Response.Cookies.Append(_cookieOptions.Name, token); diff --git a/Topers.Api/Controllers/AddressesController.cs b/Topers.Api/Controllers/AddressesController.cs index db93d52..d896ee8 100644 --- a/Topers.Api/Controllers/AddressesController.cs +++ b/Topers.Api/Controllers/AddressesController.cs @@ -16,7 +16,10 @@ public class AddressesController(IAddressesService addressesService) : Controlle [HttpPost("{customerId:guid}")] [SwaggerResponse(200, Description = "Returns the new address data of the customer.", Type = typeof(AddressResponseDto))] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> AddAddressToCustomer([FromRoute] Guid customerId, [FromBody] AddressRequestDto address) + public async Task> AddAddressToCustomer( + [FromRoute] Guid customerId, + [FromBody] AddressRequestDto address, + CancellationToken cancellationToken) { var newAddressValidator = new AddressDtoValidator(); @@ -38,7 +41,7 @@ public async Task> AddAddressToCustomer([FromRo address.Country ); - var addressEntity = await _addressService.AddAddressToCustomerAsync(newAddress); + var addressEntity = await _addressService.AddAddressToCustomerAsync(newAddress, cancellationToken); return Ok(addressEntity); } diff --git a/Topers.Api/Controllers/CategoriesController.cs b/Topers.Api/Controllers/CategoriesController.cs index 4990567..8fd8abb 100644 --- a/Topers.Api/Controllers/CategoriesController.cs +++ b/Topers.Api/Controllers/CategoriesController.cs @@ -23,9 +23,9 @@ public CategoriesController(ICategoriesService categoryService) [HttpGet] [SwaggerResponse(200, Description = "Returns a category list.", Type = typeof(IEnumerable))] [SwaggerResponse(400, Description = "Categories not found.")] - public async Task>> GetCategories() + public async Task>> GetCategories(CancellationToken cancellationToken) { - var categories = await _categoryService.GetAllCategoriesAsync(); + var categories = await _categoryService.GetAllCategoriesAsync(cancellationToken); if (categories == null) { @@ -39,9 +39,11 @@ public async Task>> GetCategories() [HttpGet("{categoryId:guid}")] [SwaggerResponse(200, Description = "Returns a category.", Type = typeof(CategoryResponseDto))] [SwaggerResponse(400, Description = "Category not found.")] - public async Task> GetCategory([FromRoute] Guid categoryId) + public async Task> GetCategory( + [FromRoute] Guid categoryId, + CancellationToken cancellationToken) { - var category = await _categoryService.GetCategoryByIdAsync(categoryId); + var category = await _categoryService.GetCategoryByIdAsync(categoryId, cancellationToken); if (category == null) { @@ -55,9 +57,11 @@ public async Task> GetCategory([FromRoute] Gui [HttpGet("{categoryId:guid}/goods")] [SwaggerResponse(200, Description = "Returns a category goods.", Type = typeof(IEnumerable))] [SwaggerResponse(400, Description = "Goods not found.")] - public async Task>> GetCategoryGoods([FromRoute] Guid categoryId) + public async Task>> GetCategoryGoods( + [FromRoute] Guid categoryId, + CancellationToken cancellationToken) { - var goods = await _categoryService.GetGoodsByCategoryIdAsync(categoryId); + var goods = await _categoryService.GetGoodsByCategoryIdAsync(categoryId, cancellationToken); if (goods == null) { @@ -70,7 +74,9 @@ public async Task>> GetCategoryGoods([FromRou [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new category.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> CreateCategory([FromBody] CategoryRequestDto category) + public async Task> CreateCategory( + [FromBody] CategoryRequestDto category, + CancellationToken cancellationToken) { var categoryValidator = new CategoryDtoValidator(); @@ -88,7 +94,7 @@ public async Task> CreateCategory([FromBody] C category.Description ); - var newCategoryEntity = await _categoryService.CreateCategoryAsync(newCategory); + var newCategoryEntity = await _categoryService.CreateCategoryAsync(newCategory, cancellationToken); return Ok(newCategoryEntity); } @@ -96,7 +102,10 @@ public async Task> CreateCategory([FromBody] C [HttpPut("{categoryId:guid}")] [SwaggerResponse(200, Description = "Update an existing category.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> UpdateCategory([FromRoute] Guid categoryId, [FromBody] CategoryRequestDto category) + public async Task> UpdateCategory( + [FromRoute] Guid categoryId, + [FromBody] CategoryRequestDto category, + CancellationToken cancellationToken) { var categoryValidator = new CategoryDtoValidator(); @@ -114,16 +123,18 @@ public async Task> UpdateCategory([FromRoute] category.Description ); - var updatedCategory = await _categoryService.UpdateCategoryAsync(existCategory); + var updatedCategory = await _categoryService.UpdateCategoryAsync(existCategory, cancellationToken); return Ok(updatedCategory); } [HttpDelete("{categoryId:guid}")] [SwaggerResponse(200, Description = "Delete category.")] - public async Task> DeleteCategory([FromRoute] Guid categoryId) + public async Task> DeleteCategory( + [FromRoute] Guid categoryId, + CancellationToken cancellationToken) { - await _categoryService.DeleteCategoryAsync(categoryId); + await _categoryService.DeleteCategoryAsync(categoryId, cancellationToken); return Ok(categoryId); } diff --git a/Topers.Api/Controllers/CustomersController.cs b/Topers.Api/Controllers/CustomersController.cs index 3959c48..35b2cbd 100644 --- a/Topers.Api/Controllers/CustomersController.cs +++ b/Topers.Api/Controllers/CustomersController.cs @@ -16,9 +16,9 @@ public class CustomersController(ICustomersService customerService) : Controller [HttpGet] [SwaggerResponse(200, Description = "Returns a customer list.", Type = typeof(IEnumerable))] [SwaggerResponse(400, Description = "Customers not found.")] - public async Task>> GetCustomers() + public async Task>> GetCustomers(CancellationToken cancellationToken) { - var customers = await _customerService.GetAllCustomersAsync(); + var customers = await _customerService.GetAllCustomersAsync(cancellationToken); if (customers == null) { @@ -31,9 +31,11 @@ public async Task>> GetCustomers() [HttpGet("{customerId:guid}")] [SwaggerResponse(200, Description = "Returns a customer.", Type = typeof(CustomerResponseDto))] [SwaggerResponse(400, Description = "Customer not found.")] - public async Task> GetCustomerById([FromRoute] Guid customerId) + public async Task> GetCustomerById( + [FromRoute] Guid customerId, + CancellationToken cancellationToken) { - var customer = await _customerService.GetCustomerByIdAsync(customerId); + var customer = await _customerService.GetCustomerByIdAsync(customerId, cancellationToken); if (customer == null) { @@ -46,7 +48,9 @@ public async Task> GetCustomerById([FromRoute] [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new customer.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> CreateCustomer([FromBody] CustomerRequestDto customer) + public async Task> CreateCustomer( + [FromBody] CustomerRequestDto customer, + CancellationToken cancellationToken) { var newCustomerValidator = new CustomerDtoValidator(); @@ -66,7 +70,7 @@ public async Task> CreateCustomer([FromBody] CustomerRequestD customer.Phone ); - var newCustomerEntity = await _customerService.CreateCustomerAsync(newCustomer); + var newCustomerEntity = await _customerService.CreateCustomerAsync(newCustomer, cancellationToken); return Ok(newCustomerEntity); } diff --git a/Topers.Api/Controllers/GoodsController.cs b/Topers.Api/Controllers/GoodsController.cs index bb55ce4..5bd4de9 100644 --- a/Topers.Api/Controllers/GoodsController.cs +++ b/Topers.Api/Controllers/GoodsController.cs @@ -49,7 +49,9 @@ public async Task> GetGood([FromRoute] Guid goodId [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new good.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> CreateGood([FromBody] GoodRequestDto good) + public async Task> CreateGood( + [FromBody] GoodRequestDto good, + CancellationToken cancellationToken) { var newGoodValidator = new GoodDtoValidator(); @@ -68,7 +70,7 @@ public async Task> CreateGood([FromBody] GoodReque good.Description ); - var newGoodEntity = await _goodService.CreateGoodAsync(newGood); + var newGoodEntity = await _goodService.CreateGoodAsync(newGood, cancellationToken); return Ok(newGoodEntity); } @@ -76,7 +78,10 @@ public async Task> CreateGood([FromBody] GoodReque [HttpPut("{goodId:guid}")] [SwaggerResponse(200, Description = "Update an existing good.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> UpdateGood([FromRoute] Guid goodId, [FromBody] GoodRequestDto good) + public async Task> UpdateGood( + [FromRoute] Guid goodId, + [FromBody] GoodRequestDto good, + CancellationToken cancellationToken) { var goodValidator = new GoodDtoValidator(); @@ -95,22 +100,27 @@ public async Task> UpdateGood([FromRoute] Guid goo good.Description ); - var updatedGood = await _goodService.UpdateGoodAsync(existGood); + var updatedGood = await _goodService.UpdateGoodAsync(existGood, cancellationToken); return Ok(updatedGood); } [HttpDelete("{goodId:guid}")] [SwaggerResponse(200, Description = "Delete good.")] - public async Task> DeleteGood([FromRoute] Guid goodId) + public async Task> DeleteGood( + [FromRoute] Guid goodId, + CancellationToken cancellationToken) { - await _goodService.DeleteGoodAsync(goodId); + await _goodService.DeleteGoodAsync(goodId, cancellationToken); return Ok(goodId); } [HttpPost("{goodId:guid}/addScope")] - public async Task> AddGoodScope([FromRoute] Guid goodId, [FromForm] GoodScopeRequestDto scope) + public async Task> AddGoodScope( + [FromRoute] Guid goodId, + [FromForm] GoodScopeRequestDto scope, + CancellationToken cancellationToken) { if (scope.ImageFile == null) { @@ -132,9 +142,9 @@ public async Task> AddGoodScope([FromRoute] Guid goodId, [Fro scope.ImageName ); - var isUpdated = await _goodService.IsGoodScopeExistsAsync(goodId, scopeModel.Litre); + var isUpdated = await _goodService.IsGoodScopeExistsAsync(goodId, scopeModel.Litre, cancellationToken); - var newScopeIdentifier = (!isUpdated) ? await _goodService.AddGoodScopeAsync(scopeModel) : await _goodService.UpdateGoodScopeAsync(scopeModel); + var newScopeIdentifier = (!isUpdated) ? await _goodService.AddGoodScopeAsync(scopeModel, cancellationToken) : await _goodService.UpdateGoodScopeAsync(scopeModel, cancellationToken); return Ok(newScopeIdentifier); } diff --git a/Topers.Api/Controllers/OrdersController.cs b/Topers.Api/Controllers/OrdersController.cs index 837fac0..45fe049 100644 --- a/Topers.Api/Controllers/OrdersController.cs +++ b/Topers.Api/Controllers/OrdersController.cs @@ -17,9 +17,9 @@ IOrdersService orderService [HttpGet] [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(IEnumerable))] [SwaggerResponse(400, Description = "Orders not found.")] - public async Task>> GetOrders() + public async Task>> GetOrders(CancellationToken cancellationToken) { - var orders = await _ordersService.GetAllOrdersAsync(); + var orders = await _ordersService.GetAllOrdersAsync(cancellationToken); if (orders == null) { @@ -32,9 +32,11 @@ public async Task>> GetOrders() [HttpGet("{orderId:guid}")] [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(OrderResponseDto))] [SwaggerResponse(400, Description = "Orders not found.")] - public async Task> GetOrderById([FromRoute] Guid orderId) + public async Task> GetOrderById( + [FromRoute] Guid orderId, + CancellationToken cancellationToken) { - var order = await _ordersService.GetOrderByIdAsync(orderId); + var order = await _ordersService.GetOrderByIdAsync(orderId, cancellationToken); if (order == null) { @@ -47,7 +49,9 @@ public async Task> GetOrderById([FromRoute] Guid [HttpPost("create")] [SwaggerResponse(200, Description = "Create a new order.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> CreateGood([FromBody] OrderRequestDto order) + public async Task> CreateGood( + [FromBody] OrderRequestDto order, + CancellationToken cancellationToken) { var newOrder = new Order ( @@ -57,7 +61,7 @@ public async Task> CreateGood([FromBody] OrderReq 0 ); - var newOrderEntity = await _ordersService.CreateOrderAsync(newOrder); + var newOrderEntity = await _ordersService.CreateOrderAsync(newOrder, cancellationToken); return Ok(newOrderEntity); } @@ -65,7 +69,10 @@ public async Task> CreateGood([FromBody] OrderReq [HttpPut("{orderId:guid}")] [SwaggerResponse(200, Description = "Update an existing order.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> UpdateOrder([FromRoute] Guid orderId, [FromBody] UpdateOrderRequestDto order) + public async Task> UpdateOrder( + [FromRoute] Guid orderId, + [FromBody] UpdateOrderRequestDto order, + CancellationToken cancellationToken) { var existOrder = new Order ( @@ -75,7 +82,7 @@ public async Task> UpdateOrder([FromRoute] Guid o Decimal.MinValue ); - var updatedOrder = await _ordersService.UpdateOrderAsync(existOrder); + var updatedOrder = await _ordersService.UpdateOrderAsync(existOrder, cancellationToken); return Ok(updatedOrder); } @@ -83,7 +90,10 @@ public async Task> UpdateOrder([FromRoute] Guid o [HttpPost("{orderId:guid}/addGood")] [SwaggerResponse(200, Description = "Add good to an existing order.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] - public async Task> AddProductToOrder([FromRoute] Guid orderId, [FromBody] AddProductToOrderRequestDto orderDetail) + public async Task> AddProductToOrder( + [FromRoute] Guid orderId, + [FromBody] AddProductToOrderRequestDto orderDetail, + CancellationToken cancellationToken) { var newGoodDetail = new OrderDetails ( @@ -99,7 +109,7 @@ public async Task> AddProductToOrder([FromRoute] orderDetail.GoodLitre ); - var newGoodDetailIdentifier = await _ordersService.AddGoodToOrderAsync(newGoodDetail, newGoodScope); + var newGoodDetailIdentifier = await _ordersService.AddGoodToOrderAsync(newGoodDetail, newGoodScope, cancellationToken); return Ok(newGoodDetailIdentifier); } diff --git a/Topers.Api/Middleware/TaskCancellationHandlingMiddleware.cs b/Topers.Api/Middleware/TaskCancellationHandlingMiddleware.cs new file mode 100644 index 0000000..62c144b --- /dev/null +++ b/Topers.Api/Middleware/TaskCancellationHandlingMiddleware.cs @@ -0,0 +1,25 @@ +namespace Topers.Api.Middleware; + +public class TaskCancellationHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public TaskCancellationHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception _) when (_ is OperationCanceledException or TaskCanceledException) + { + _logger.LogInformation("Task has been cancelled!"); + } + } +} \ No newline at end of file diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index e3aefe3..2104fa7 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.FileProviders; using System.Globalization; using Microsoft.AspNetCore.Localization; +using Topers.Api.Middleware; var builder = WebApplication.CreateBuilder(args); { @@ -122,5 +123,9 @@ app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); + app.MapGet("get", () => { + Results.Ok("ok"); + }); + app.UseMiddleware(); app.Run(); } \ No newline at end of file diff --git a/Topers.Core/Abstractions/IAddressesRepository.cs b/Topers.Core/Abstractions/IAddressesRepository.cs index 780f953..b36aff5 100644 --- a/Topers.Core/Abstractions/IAddressesRepository.cs +++ b/Topers.Core/Abstractions/IAddressesRepository.cs @@ -4,5 +4,5 @@ namespace Topers.Core.Abstractions; public interface IAddressesRepository { - Task CreateAsync(Address address); + Task CreateAsync(Address address, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IAddressesService.cs b/Topers.Core/Abstractions/IAddressesService.cs index bfa939a..e428ccd 100644 --- a/Topers.Core/Abstractions/IAddressesService.cs +++ b/Topers.Core/Abstractions/IAddressesService.cs @@ -5,5 +5,5 @@ namespace Topers.Core.Abstractions; public interface IAddressesService { - Task AddAddressToCustomerAsync(Address address); + Task AddAddressToCustomerAsync(Address address, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICategoriesRespository.cs b/Topers.Core/Abstractions/ICategoriesRespository.cs index f0d6d3e..6011fae 100644 --- a/Topers.Core/Abstractions/ICategoriesRespository.cs +++ b/Topers.Core/Abstractions/ICategoriesRespository.cs @@ -4,10 +4,10 @@ namespace Topers.Core.Abstractions; public interface ICategoriesRepository { - Task CreateAsync(Category category); - Task> GetAllAsync(); - Task GetByIdAsync(Guid categoryId); - Task> GetGoodsByIdAsync(Guid categoryId); - Task UpdateAsync(Category category); - Task DeleteAsync(Guid categoryId); + Task CreateAsync(Category category, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid categoryId, CancellationToken cancellationToken = default); + Task> GetGoodsByIdAsync(Guid categoryId, CancellationToken cancellationToken = default); + Task UpdateAsync(Category category, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid categoryId, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICategoriesService.cs b/Topers.Core/Abstractions/ICategoriesService.cs index 247de09..b49855e 100644 --- a/Topers.Core/Abstractions/ICategoriesService.cs +++ b/Topers.Core/Abstractions/ICategoriesService.cs @@ -5,10 +5,10 @@ namespace Topers.Core.Abstractions; public interface ICategoriesService { - Task CreateCategoryAsync(Category category); - Task> GetAllCategoriesAsync(); - Task GetCategoryByIdAsync(Guid categoryId); - Task> GetGoodsByCategoryIdAsync(Guid categoryId); - Task UpdateCategoryAsync(Category category); - Task DeleteCategoryAsync(Guid categoryId); + Task CreateCategoryAsync(Category category, CancellationToken cancellationToken = default); + Task> GetAllCategoriesAsync(CancellationToken cancellationToken = default); + Task GetCategoryByIdAsync(Guid categoryId, CancellationToken cancellationToken = default); + Task> GetGoodsByCategoryIdAsync(Guid categoryId, CancellationToken cancellationToken = default); + Task UpdateCategoryAsync(Category category, CancellationToken cancellationToken = default); + Task DeleteCategoryAsync(Guid categoryId, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICustomersRepository.cs b/Topers.Core/Abstractions/ICustomersRepository.cs index 45b82dd..34c68b2 100644 --- a/Topers.Core/Abstractions/ICustomersRepository.cs +++ b/Topers.Core/Abstractions/ICustomersRepository.cs @@ -4,7 +4,7 @@ namespace Topers.Core.Abstractions; public interface ICustomersRepository { - Task> GetAllAsync(); - Task GetByIdAsync(Guid customerId); - Task CreateAsync(Customer customer); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid customerId, CancellationToken cancellationToken = default); + Task CreateAsync(Customer customer, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICustomersService.cs b/Topers.Core/Abstractions/ICustomersService.cs index dd8fb69..2a01e36 100644 --- a/Topers.Core/Abstractions/ICustomersService.cs +++ b/Topers.Core/Abstractions/ICustomersService.cs @@ -5,7 +5,7 @@ namespace Topers.Core.Abstractions; public interface ICustomersService { - Task> GetAllCustomersAsync(); - Task GetCustomerByIdAsync(Guid customerId); - Task CreateCustomerAsync(Customer customer); + Task> GetAllCustomersAsync(CancellationToken cancellationToken = default); + Task GetCustomerByIdAsync(Guid customerId, CancellationToken cancellationToken = default); + Task CreateCustomerAsync(Customer customer, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsRepository.cs b/Topers.Core/Abstractions/IGoodsRepository.cs index c1c3c39..5a734b9 100644 --- a/Topers.Core/Abstractions/IGoodsRepository.cs +++ b/Topers.Core/Abstractions/IGoodsRepository.cs @@ -4,13 +4,13 @@ namespace Topers.Core.Abstractions; public interface IGoodsRepository { - Task CreateAsync(Good good); - Task> GetAllAsync(); - Task GetByIdAsync(Guid goodId); - Task> GetByFilterAsync(string title); - Task UpdateAsync(Good good); - Task DeleteAsync(Guid goodId); - Task GetScopeAsync(Guid goodId, int litre); - Task AddScopeAsync(GoodScope goodScope); - Task UpdateScopeAsync(GoodScope goodScope); + Task CreateAsync(Good good, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid goodId, CancellationToken cancellationToken = default); + Task> GetByFilterAsync(string title, CancellationToken cancellationToken = default); + Task UpdateAsync(Good good, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid goodId, CancellationToken cancellationToken = default); + Task GetScopeAsync(Guid goodId, int litre, CancellationToken cancellationToken = default); + Task AddScopeAsync(GoodScope goodScope, CancellationToken cancellationToken = default); + Task UpdateScopeAsync(GoodScope goodScope, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IGoodsService.cs b/Topers.Core/Abstractions/IGoodsService.cs index 0e898aa..101cb4e 100644 --- a/Topers.Core/Abstractions/IGoodsService.cs +++ b/Topers.Core/Abstractions/IGoodsService.cs @@ -5,12 +5,12 @@ namespace Topers.Core.Abstractions; public interface IGoodsService { - Task CreateGoodAsync(Good good); - Task> GetAllGoodsAsync(); - Task GetGoodByIdAsync(Guid goodId); - Task UpdateGoodAsync(Good good); - Task DeleteGoodAsync(Guid goodId); - Task AddGoodScopeAsync(GoodScope scope); - Task UpdateGoodScopeAsync(GoodScope scope); - Task IsGoodScopeExistsAsync(Guid goodId, int litre); + Task CreateGoodAsync(Good good, CancellationToken cancellationToken = default); + Task> GetAllGoodsAsync(CancellationToken cancellationToken = default); + Task GetGoodByIdAsync(Guid goodId, CancellationToken cancellationToken = default); + Task UpdateGoodAsync(Good good, CancellationToken cancellationToken = default); + Task DeleteGoodAsync(Guid goodId, CancellationToken cancellationToken = default); + Task AddGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default); + Task UpdateGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default); + Task IsGoodScopeExistsAsync(Guid goodId, int litre, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IOrdersRepository.cs b/Topers.Core/Abstractions/IOrdersRepository.cs index 9634604..9da6def 100644 --- a/Topers.Core/Abstractions/IOrdersRepository.cs +++ b/Topers.Core/Abstractions/IOrdersRepository.cs @@ -4,10 +4,10 @@ namespace Topers.Core.Abstractions; public interface IOrdersRepository { - Task> GetAllAsync(); - Task GetByIdAsync(Guid orderId); - Task CreateAsync(Order order); - Task UpdateAsync(Order order); - Task DeleteAsync(Guid orderId); - Task AddDetailAsync(OrderDetails detail); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task GetByIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task CreateAsync(Order order, CancellationToken cancellationToken = default); + Task UpdateAsync(Order order, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid orderId, CancellationToken cancellationToken = default); + Task AddDetailAsync(OrderDetails detail, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IOrdersService.cs b/Topers.Core/Abstractions/IOrdersService.cs index 65613f0..0a02b6b 100644 --- a/Topers.Core/Abstractions/IOrdersService.cs +++ b/Topers.Core/Abstractions/IOrdersService.cs @@ -5,10 +5,10 @@ namespace Topers.Core.Abstractions; public interface IOrdersService { - Task> GetAllOrdersAsync(); - Task GetOrderByIdAsync(Guid orderId); - Task CreateOrderAsync(Order order); - Task UpdateOrderAsync(Order order); - Task DeleteOrderAsync(Guid orderId); - Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good); + Task> GetAllOrdersAsync(CancellationToken cancellationToken = default); + Task GetOrderByIdAsync(Guid orderId, CancellationToken cancellationToken = default); + Task CreateOrderAsync(Order order, CancellationToken cancellationToken = default); + Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default); + Task DeleteOrderAsync(Guid orderId, CancellationToken cancellationToken = default); + Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IUsersRepository.cs b/Topers.Core/Abstractions/IUsersRepository.cs index 142b7f7..7025124 100644 --- a/Topers.Core/Abstractions/IUsersRepository.cs +++ b/Topers.Core/Abstractions/IUsersRepository.cs @@ -4,6 +4,6 @@ namespace Topers.Core.Abstractions; public interface IUsersRepository { - Task Add(User user); - Task GetByName(string username); + Task Add(User user, CancellationToken cancellationToken = default); + Task GetByName(string username, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.Core/Abstractions/IUsersService.cs b/Topers.Core/Abstractions/IUsersService.cs index a0a4737..29dc3ae 100644 --- a/Topers.Core/Abstractions/IUsersService.cs +++ b/Topers.Core/Abstractions/IUsersService.cs @@ -2,6 +2,6 @@ namespace Topers.Core.Abstractions; public interface IUsersService { - Task Register(string username, string email, string password); - Task Login(string username, string password); + Task Register(string username, string email, string password, CancellationToken cancellationToken = default); + Task Login(string username, string password, CancellationToken cancellationToken = default); }; \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs index 7eb2d3b..472ef65 100644 --- a/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/AddressesRepository.cs @@ -14,7 +14,7 @@ public AddressesRepository(TopersDbContext context) _context = context; } - public async Task CreateAsync(Address address) + public async Task CreateAsync(Address address, CancellationToken cancellationToken = default) { var addressEntity = new AddressEntity { diff --git a/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs b/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs index 5ab67ad..1c1a8bc 100644 --- a/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/CategoriesRepository.cs @@ -17,7 +17,7 @@ public CategoriesRepository(TopersDbContext context, IMapper mapper) _mapper = mapper; } - public async Task CreateAsync(Category category) + public async Task CreateAsync(Category category, CancellationToken cancellationToken = default) { var categoryEntity = new CategoryEntity { @@ -32,7 +32,7 @@ public async Task CreateAsync(Category category) return categoryEntity.Id; } - public async Task DeleteAsync(Guid categoryId) + public async Task DeleteAsync(Guid categoryId, CancellationToken cancellationToken = default) { await _context.Categories .Where(c => c.Id == categoryId) @@ -41,7 +41,7 @@ await _context.Categories return categoryId; } - public async Task> GetAllAsync() + public async Task> GetAllAsync(CancellationToken cancellationToken = default) { var categoryEntities = await _context.Categories .AsNoTracking() @@ -52,7 +52,7 @@ public async Task> GetAllAsync() return categoryEntitiesDto; } - public async Task GetByIdAsync(Guid categoryId) + public async Task GetByIdAsync(Guid categoryId, CancellationToken cancellationToken = default) { var categoryEntity = await _context.Categories .AsNoTracking() @@ -63,7 +63,7 @@ public async Task GetByIdAsync(Guid categoryId) return categoryEntityDto; } - public async Task> GetGoodsByIdAsync(Guid categoryId) + public async Task> GetGoodsByIdAsync(Guid categoryId, CancellationToken cancellationToken = default) { var goodEntities = await _context.Goods .Where(g => g.CategoryId == categoryId) @@ -74,7 +74,7 @@ public async Task> GetGoodsByIdAsync(Guid categoryId) return goodEntitiesDto; } - public async Task UpdateAsync(Category category) + public async Task UpdateAsync(Category category, CancellationToken cancellationToken = default) { await _context.Categories .Where(c => c.Id == category.Id) diff --git a/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs b/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs index 2502010..7d7e977 100644 --- a/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/CustomersRepository.cs @@ -17,7 +17,7 @@ public CustomersRepository(TopersDbContext context, IMapper mapper) _mapper = mapper; } - public async Task CreateAsync(Customer customer) + public async Task CreateAsync(Customer customer, CancellationToken cancellationToken = default) { var customerEntity = new CustomerEntity { @@ -33,7 +33,7 @@ public async Task CreateAsync(Customer customer) return customerEntity.Id; } - public async Task> GetAllAsync() + public async Task> GetAllAsync(CancellationToken cancellationToken = default) { var customerEntities = await _context.Customers .Include(c => c.Address) @@ -45,7 +45,7 @@ public async Task> GetAllAsync() return customerEntitiesDto; } - public async Task GetByIdAsync(Guid customerId) + public async Task GetByIdAsync(Guid customerId, CancellationToken cancellationToken = default) { var customerEntity = await _context.Customers .Include(c => c.Address) diff --git a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs index 2987bc2..c8263a9 100644 --- a/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/GoodsRepository.cs @@ -17,7 +17,7 @@ public GoodsRepository(TopersDbContext context, IMapper mapper) _mapper = mapper; } - public async Task CreateAsync(Good good) + public async Task CreateAsync(Good good, CancellationToken cancellationToken = default) { var goodEntity = new GoodEntity { @@ -33,7 +33,7 @@ public async Task CreateAsync(Good good) return goodEntity.Id; } - public async Task DeleteAsync(Guid goodId) + public async Task DeleteAsync(Guid goodId, CancellationToken cancellationToken = default) { await _context.Goods .Where(g => g.Id == goodId) @@ -42,7 +42,7 @@ await _context.Goods return goodId; } - public async Task> GetAllAsync() + public async Task> GetAllAsync(CancellationToken cancellationToken = default) { var goodEntities = await _context.Goods .Include(g => g.Scopes) @@ -54,7 +54,7 @@ public async Task> GetAllAsync() return goodEntitiesDto; } - public async Task> GetByFilterAsync(string title) + public async Task> GetByFilterAsync(string title, CancellationToken cancellationToken = default) { var query = _context.Goods.Include(g => g.Scopes).AsNoTracking(); @@ -68,7 +68,7 @@ public async Task> GetByFilterAsync(string title) return goodEntitiesDto; } - public async Task GetByIdAsync(Guid goodId) + public async Task GetByIdAsync(Guid goodId, CancellationToken cancellationToken = default) { var goodEntity = await _context.Goods .AsNoTracking() @@ -79,7 +79,7 @@ public async Task GetByIdAsync(Guid goodId) return goodEntityDto; } - public async Task UpdateAsync(Good good) + public async Task UpdateAsync(Good good, CancellationToken cancellationToken = default) { await _context.Goods .Where(g => g.Id == good.Id) @@ -91,7 +91,7 @@ await _context.Goods return good.Id; } - public async Task GetScopeAsync(Guid goodId, int litre) + public async Task GetScopeAsync(Guid goodId, int litre, CancellationToken cancellationToken = default) { var pExistsGoodScope = await _context.GoodScopes .FirstOrDefaultAsync(gs => gs.GoodId == goodId && gs.Litre == litre); @@ -99,7 +99,7 @@ public async Task GetScopeAsync(Guid goodId, int litre) return _mapper.Map(pExistsGoodScope); } - public async Task AddScopeAsync(GoodScope goodScope) + public async Task AddScopeAsync(GoodScope goodScope, CancellationToken cancellationToken = default) { var scopeEntity = new GoodScopeEntity { @@ -116,7 +116,7 @@ public async Task AddScopeAsync(GoodScope goodScope) return scopeEntity.Id; } - public async Task UpdateScopeAsync(GoodScope goodScope) + public async Task UpdateScopeAsync(GoodScope goodScope, CancellationToken cancellationToken = default) { var existingGoodScope = await _context.GoodScopes .AsNoTracking() diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs index 79853c5..4f32c77 100644 --- a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -17,7 +17,7 @@ public OrdersRepository(TopersDbContext context, IMapper mapper) _mapper = mapper; } - public async Task CreateAsync(Order order) + public async Task CreateAsync(Order order, CancellationToken cancellationToken = default) { var orderEntity = new OrderEntity { @@ -33,7 +33,7 @@ public async Task CreateAsync(Order order) return orderEntity.Id; } - public async Task DeleteAsync(Guid orderId) + public async Task DeleteAsync(Guid orderId, CancellationToken cancellationToken = default) { await _context.Orders .Where(o => o.Id == orderId) @@ -42,7 +42,7 @@ await _context.Orders return orderId; } - public async Task> GetAllAsync() + public async Task> GetAllAsync(CancellationToken cancellationToken = default) { var orderEntities = await _context.Orders .Include(o => o.OrderDetails) @@ -54,7 +54,7 @@ public async Task> GetAllAsync() return orders; } - public async Task GetByIdAsync(Guid orderId) + public async Task GetByIdAsync(Guid orderId, CancellationToken cancellationToken = default) { var orderEntity = await _context.Orders .Include(o => o.OrderDetails) @@ -66,7 +66,7 @@ public async Task GetByIdAsync(Guid orderId) return orderEntityDto; } - public async Task UpdateAsync(Order order) + public async Task UpdateAsync(Order order, CancellationToken cancellationToken = default) { await _context.Orders .Where(o => o.Id == order.Id) @@ -76,7 +76,7 @@ await _context.Orders return order.Id; } - public async Task AddDetailAsync(OrderDetails detail) + public async Task AddDetailAsync(OrderDetails detail, CancellationToken cancellationToken = default) { var orderDetailEntity = new OrderDetailsEntity { diff --git a/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs b/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs index a0866bf..2e8a3f3 100644 --- a/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/UsersRepository.cs @@ -18,7 +18,7 @@ public UsersRepository(TopersDbContext context, IMapper mapper) _mapper = mapper; } - public async Task Add(User user) + public async Task Add(User user, CancellationToken cancellationToken = default) { var userEntity = new UserEntity() { @@ -32,7 +32,7 @@ public async Task Add(User user) await _context.SaveChangesAsync(); } - public async Task GetByName(string username) + public async Task GetByName(string username, CancellationToken cancellationToken = default) { var userEntity = await _context.Users .AsNoTracking() diff --git a/Topers.Infrastructure/Services/AddressesService.cs b/Topers.Infrastructure/Services/AddressesService.cs index e650fd3..0859eac 100644 --- a/Topers.Infrastructure/Services/AddressesService.cs +++ b/Topers.Infrastructure/Services/AddressesService.cs @@ -13,7 +13,7 @@ public AddressesService(IAddressesRepository addressesRepository) _addressesRepository = addressesRepository; } - public async Task AddAddressToCustomerAsync(Address address) + public async Task AddAddressToCustomerAsync(Address address, CancellationToken cancellationToken = default) { var addressEntityIdentifier = await _addressesRepository.CreateAsync(address); diff --git a/Topers.Infrastructure/Services/CategoriesService.cs b/Topers.Infrastructure/Services/CategoriesService.cs index 83c2432..060b22a 100644 --- a/Topers.Infrastructure/Services/CategoriesService.cs +++ b/Topers.Infrastructure/Services/CategoriesService.cs @@ -16,7 +16,7 @@ public CategoriesService(ICategoriesRepository categoriesRepository, IMapper map _mapper = mapper; } - public async Task CreateCategoryAsync(Category category) + public async Task CreateCategoryAsync(Category category, CancellationToken cancellationToken = default) { var newCategoryIdentifier = await _categoriesRepository.CreateAsync(category); @@ -30,27 +30,27 @@ public async Task CreateCategoryAsync(Category category) return newCategory; } - public async Task DeleteCategoryAsync(Guid categoryId) + public async Task DeleteCategoryAsync(Guid categoryId, CancellationToken cancellationToken = default) { return await _categoriesRepository.DeleteAsync(categoryId); } - public async Task> GetAllCategoriesAsync() + public async Task> GetAllCategoriesAsync(CancellationToken cancellationToken = default) { return _mapper.Map>(await _categoriesRepository.GetAllAsync()); } - public async Task GetCategoryByIdAsync(Guid categoryId) + public async Task GetCategoryByIdAsync(Guid categoryId, CancellationToken cancellationToken = default) { return _mapper.Map(await _categoriesRepository.GetByIdAsync(categoryId)); } - public async Task> GetGoodsByCategoryIdAsync(Guid categoryId) + public async Task> GetGoodsByCategoryIdAsync(Guid categoryId, CancellationToken cancellationToken = default) { return _mapper.Map>(await _categoriesRepository.GetGoodsByIdAsync(categoryId)); } - public async Task UpdateCategoryAsync(Category category) + public async Task UpdateCategoryAsync(Category category, CancellationToken cancellationToken = default) { return await _categoriesRepository.UpdateAsync(category); } diff --git a/Topers.Infrastructure/Services/CustomersService.cs b/Topers.Infrastructure/Services/CustomersService.cs index 5502b33..c964b57 100644 --- a/Topers.Infrastructure/Services/CustomersService.cs +++ b/Topers.Infrastructure/Services/CustomersService.cs @@ -16,7 +16,7 @@ public CustomersService(ICustomersRepository customersRepository, IMapper mapper _mapper = mapper; } - public async Task CreateCustomerAsync(Customer customer) + public async Task CreateCustomerAsync(Customer customer, CancellationToken cancellationToken = default) { var newCustomerIdentifier = await _customersRepository.CreateAsync(customer); @@ -31,12 +31,12 @@ public async Task CreateCustomerAsync(Customer customer) return newCustomer; } - public async Task> GetAllCustomersAsync() + public async Task> GetAllCustomersAsync(CancellationToken cancellationToken = default) { return _mapper.Map>(await _customersRepository.GetAllAsync()); } - public async Task GetCustomerByIdAsync(Guid customerId) + public async Task GetCustomerByIdAsync(Guid customerId, CancellationToken cancellationToken = default) { return _mapper.Map(await _customersRepository.GetByIdAsync(customerId)); } diff --git a/Topers.Infrastructure/Services/GoodsService.cs b/Topers.Infrastructure/Services/GoodsService.cs index c4637c5..559e0ad 100644 --- a/Topers.Infrastructure/Services/GoodsService.cs +++ b/Topers.Infrastructure/Services/GoodsService.cs @@ -16,7 +16,7 @@ public GoodsService(IGoodsRepository goodsRepository, IMapper mapper) _mapper = mapper; } - public async Task CreateGoodAsync(Good good) + public async Task CreateGoodAsync(Good good, CancellationToken cancellationToken = default) { var newGoodIdentifier = await _goodsRepository.CreateAsync(good); @@ -31,37 +31,37 @@ public async Task CreateGoodAsync(Good good) return newGood; } - public async Task DeleteGoodAsync(Guid goodId) + public async Task DeleteGoodAsync(Guid goodId, CancellationToken cancellationToken = default) { return await _goodsRepository.DeleteAsync(goodId); } - public async Task> GetAllGoodsAsync() + public async Task> GetAllGoodsAsync(CancellationToken cancellationToken = default) { return _mapper.Map>(await _goodsRepository.GetAllAsync()); } - public async Task GetGoodByIdAsync(Guid goodId) + public async Task GetGoodByIdAsync(Guid goodId, CancellationToken cancellationToken = default) { return _mapper.Map(await _goodsRepository.GetByIdAsync(goodId)); } - public async Task UpdateGoodAsync(Good good) + public async Task UpdateGoodAsync(Good good, CancellationToken cancellationToken = default) { return await _goodsRepository.UpdateAsync(good); } - public async Task AddGoodScopeAsync(GoodScope scope) + public async Task AddGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default) { return await _goodsRepository.AddScopeAsync(scope); } - public async Task UpdateGoodScopeAsync(GoodScope scope) + public async Task UpdateGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default) { return await _goodsRepository.UpdateScopeAsync(scope); } - public async Task IsGoodScopeExistsAsync(Guid goodId, int litre) + public async Task IsGoodScopeExistsAsync(Guid goodId, int litre, CancellationToken cancellationToken = default) { var existingScope = await _goodsRepository.GetScopeAsync(goodId, litre); diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs index d22fcb2..229dadf 100644 --- a/Topers.Infrastructure/Services/OrdersService.cs +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -21,7 +21,7 @@ public OrdersService( _mapper = mapper; } - public async Task CreateOrderAsync(Order order) + public async Task CreateOrderAsync(Order order, CancellationToken cancellationToken = default) { var newOrderIdentifier = await _ordersRepository.CreateAsync(order); @@ -36,27 +36,27 @@ public async Task CreateOrderAsync(Order order) return newOrder; } - public async Task DeleteOrderAsync(Guid orderId) + public async Task DeleteOrderAsync(Guid orderId, CancellationToken cancellationToken = default) { return await _ordersRepository.DeleteAsync(orderId); } - public async Task> GetAllOrdersAsync() + public async Task> GetAllOrdersAsync(CancellationToken cancellationToken = default) { return _mapper.Map>(await _ordersRepository.GetAllAsync()); } - public async Task GetOrderByIdAsync(Guid orderId) + public async Task GetOrderByIdAsync(Guid orderId, CancellationToken cancellationToken = default) { return _mapper.Map(await _ordersRepository.GetByIdAsync(orderId)); } - public async Task UpdateOrderAsync(Order order) + public async Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default) { return await _ordersRepository.UpdateAsync(order); } - public async Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good) + public async Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good, CancellationToken cancellationToken = default) { var goodScope = await _goodsRepository.GetScopeAsync(detail.GoodId, good.Litre); diff --git a/Topers.Infrastructure/Services/UsersService.cs b/Topers.Infrastructure/Services/UsersService.cs index f3f8e84..4a5a483 100644 --- a/Topers.Infrastructure/Services/UsersService.cs +++ b/Topers.Infrastructure/Services/UsersService.cs @@ -20,7 +20,7 @@ public UsersService( _jwtProvider = jwtProvider; } - public async Task Register(string username, string email, string password) + public async Task Register(string username, string email, string password, CancellationToken cancellationToken = default) { var hashedPassword = _passwordHasher.Generate(password); @@ -29,7 +29,7 @@ public async Task Register(string username, string email, string password) await _usersRepository.Add(newUser); } - public async Task Login(string username, string password) + public async Task Login(string username, string password, CancellationToken cancellationToken = default) { var user = await _usersRepository.GetByName(username); From b0a48fcba4f03f7e4348eb0b6cf0d43152932fd7 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sat, 27 Jul 2024 07:57:32 +0300 Subject: [PATCH 33/39] Separate extensions. Update Program.cs --- .../Extensions/AuthenticationExtensions.cs | 44 ++++++ Topers.Api/Extensions/MiddlewareExtensions.cs | 56 ++++++++ Topers.Api/Extensions/ServiceExtensions.cs | 44 ++++++ Topers.Api/Program.cs | 132 ++---------------- 4 files changed, 155 insertions(+), 121 deletions(-) create mode 100644 Topers.Api/Extensions/AuthenticationExtensions.cs create mode 100644 Topers.Api/Extensions/MiddlewareExtensions.cs create mode 100644 Topers.Api/Extensions/ServiceExtensions.cs diff --git a/Topers.Api/Extensions/AuthenticationExtensions.cs b/Topers.Api/Extensions/AuthenticationExtensions.cs new file mode 100644 index 0000000..c2751cb --- /dev/null +++ b/Topers.Api/Extensions/AuthenticationExtensions.cs @@ -0,0 +1,44 @@ +namespace Topers.Api.Extenstions; + +using Topers.Infrastructure.Features; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Text; + +public static class AuthenticationExtensions +{ + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + var jwtOptionsSection = configuration.GetSection(nameof(JwtOptions)); + var cookieOptionsSection = configuration.GetSection(nameof(CookiesOptions)); + var cookieName = cookieOptionsSection.GetValue("Name")!; + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtOptionsSection.GetValue("SecretKey")!)) + }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + context.Token = context.Request.Cookies[cookieName]; + return Task.CompletedTask; + } + }; + }); + + return services; + } +}; \ No newline at end of file diff --git a/Topers.Api/Extensions/MiddlewareExtensions.cs b/Topers.Api/Extensions/MiddlewareExtensions.cs new file mode 100644 index 0000000..d5603e9 --- /dev/null +++ b/Topers.Api/Extensions/MiddlewareExtensions.cs @@ -0,0 +1,56 @@ +namespace Topers.Api.Extenstions; + +using System.Globalization; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.FileProviders; +using Microsoft.AspNetCore.CookiePolicy; +using Topers.Api.Middleware; + +public static class MiddlewareExtensions +{ + public static IApplicationBuilder UseCustomMiddlewares(this IApplicationBuilder application, IWebHostEnvironment environment) + { + var defaultCulture = new CultureInfo("en-US"); + var localizationOptions = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture(defaultCulture), + SupportedCultures = new[] { defaultCulture }, + SupportedUICultures = new[] { defaultCulture } + }; + + if (environment.IsDevelopment()) + { + application.UseSwagger(); + application.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); + options.DocumentTitle = "Topers API"; + options.RoutePrefix = string.Empty; + }); + } + + application.UseHttpsRedirection(); + application.UseRequestLocalization(localizationOptions); + application.UseStaticFiles(new StaticFileOptions + { + FileProvider = new PhysicalFileProvider( + Path.Combine(environment.ContentRootPath, "Uploads") + ), + RequestPath = "/Resources" + }); + application.UseCookiePolicy(new CookiePolicyOptions + { + MinimumSameSitePolicy = SameSiteMode.Strict, + HttpOnly = HttpOnlyPolicy.Always, + Secure = CookieSecurePolicy.Always + }); + application.UseCors(options => options.WithOrigins("http://localhost:5173") + .AllowAnyHeader() + .AllowAnyMethod()); + application.UseAuthentication(); + application.UseAuthorization(); + application.UseMiddleware(); + + return application; + } +}; \ No newline at end of file diff --git a/Topers.Api/Extensions/ServiceExtensions.cs b/Topers.Api/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..ca831d2 --- /dev/null +++ b/Topers.Api/Extensions/ServiceExtensions.cs @@ -0,0 +1,44 @@ +namespace Topers.Api.Extenstions; + +using Topers.Infrastructure.Features; +using Topers.DataAccess.Postgres; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.DataAccess.Postgres.Repositories; +using Topers.Infrastructure.Services; + +public static class ServiceExtensions +{ + public static IServiceCollection AddCustomServices(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(nameof(JwtOptions))); + services.Configure(configuration.GetSection(nameof(CookiesOptions))); + + services.AddDbContext(options => + { + options.UseNpgsql(configuration.GetConnectionString("TopersDbContext")); + }); + + services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +}; \ No newline at end of file diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index 2104fa7..e4ea7c9 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -1,131 +1,21 @@ -using Microsoft.EntityFrameworkCore; -using Topers.Core.Abstractions; -using Topers.DataAccess.Postgres; -using Topers.DataAccess.Postgres.Repositories; -using Topers.Infrastructure.Features; -using Topers.Infrastructure.Services; -using Microsoft.IdentityModel.Tokens; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using System.Text; -using Microsoft.AspNetCore.CookiePolicy; -using Microsoft.Extensions.FileProviders; -using System.Globalization; -using Microsoft.AspNetCore.Localization; -using Topers.Api.Middleware; +using Topers.Api.Extenstions; var builder = WebApplication.CreateBuilder(args); { - var jwtOptionsSection = builder.Configuration.GetSection(nameof(JwtOptions)); - var cookieOptionsSection = builder.Configuration.GetSection(nameof(CookiesOptions)); - var cookieName = cookieOptionsSection.GetValue("Name")!; - - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); - builder.Services.AddControllers(); - - builder.Services.Configure(jwtOptionsSection); - builder.Services.Configure(cookieOptionsSection); - - builder.Services.AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }).AddJwtBearer(options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = false, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey( - Encoding.UTF8.GetBytes(jwtOptionsSection.GetValue("SecretKey")!)) - }; - - options.Events = new JwtBearerEvents - { - OnMessageReceived = context => - { - context.Token = context.Request.Cookies[cookieName]; - - return Task.CompletedTask; - } - }; - }); - - builder.Services.AddAuthorization(); - - builder.Services.AddDbContext(options => - { - options.UseNpgsql(builder.Configuration.GetConnectionString("TopersDbContext")); - }); - - builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); - - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); -}; - -var defaultCulture = new CultureInfo("en-US"); -var localizationOptions = new RequestLocalizationOptions -{ - DefaultRequestCulture = new RequestCulture(defaultCulture), - SupportedCultures = [defaultCulture], - SupportedUICultures = [defaultCulture] + var configuration = builder.Configuration; + var services = builder.Services; + + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(); + services.AddControllers(); + services.AddCustomServices(configuration); + services.AddCustomAuthentication(configuration); + services.AddAuthorization(); }; var app = builder.Build(); { - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); - options.DocumentTitle = "Topers API"; - options.RoutePrefix = string.Empty; - }); - } - - app.UseHttpsRedirection(); - app.UseRequestLocalization(localizationOptions); - app.UseStaticFiles(new StaticFileOptions - { - FileProvider = new PhysicalFileProvider( - Path.Combine(builder.Environment.ContentRootPath, "Uploads") - ), - RequestPath = "/Resources" - }); - app.UseCookiePolicy(new CookiePolicyOptions - { - MinimumSameSitePolicy = SameSiteMode.Strict, - HttpOnly = HttpOnlyPolicy.Always, - Secure = CookieSecurePolicy.Always - }); - app.UseCors(options => options.WithOrigins("http://localhost:5173") - .AllowAnyHeader() - .AllowAnyMethod()); - app.UseAuthentication(); - app.UseAuthorization(); + app.UseCustomMiddlewares(app.Environment); app.MapControllers(); - app.MapGet("get", () => { - Results.Ok("ok"); - }); - app.UseMiddleware(); app.Run(); } \ No newline at end of file From 1b986309afb3b1f9f94c154d8652ae982d9bdd89 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sat, 27 Jul 2024 08:57:57 +0300 Subject: [PATCH 34/39] Fix spelling bugs --- Topers.Api/Extensions/AuthenticationExtensions.cs | 2 +- Topers.Api/Extensions/MiddlewareExtensions.cs | 2 +- Topers.Api/Extensions/ServiceExtensions.cs | 2 +- Topers.Api/Program.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Topers.Api/Extensions/AuthenticationExtensions.cs b/Topers.Api/Extensions/AuthenticationExtensions.cs index c2751cb..5bfa46c 100644 --- a/Topers.Api/Extensions/AuthenticationExtensions.cs +++ b/Topers.Api/Extensions/AuthenticationExtensions.cs @@ -1,4 +1,4 @@ -namespace Topers.Api.Extenstions; +namespace Topers.Api.Extensions; using Topers.Infrastructure.Features; using Microsoft.IdentityModel.Tokens; diff --git a/Topers.Api/Extensions/MiddlewareExtensions.cs b/Topers.Api/Extensions/MiddlewareExtensions.cs index d5603e9..98cdae0 100644 --- a/Topers.Api/Extensions/MiddlewareExtensions.cs +++ b/Topers.Api/Extensions/MiddlewareExtensions.cs @@ -1,4 +1,4 @@ -namespace Topers.Api.Extenstions; +namespace Topers.Api.Extensions; using System.Globalization; using Microsoft.AspNetCore.Localization; diff --git a/Topers.Api/Extensions/ServiceExtensions.cs b/Topers.Api/Extensions/ServiceExtensions.cs index ca831d2..73ea0dd 100644 --- a/Topers.Api/Extensions/ServiceExtensions.cs +++ b/Topers.Api/Extensions/ServiceExtensions.cs @@ -1,4 +1,4 @@ -namespace Topers.Api.Extenstions; +namespace Topers.Api.Extensions; using Topers.Infrastructure.Features; using Topers.DataAccess.Postgres; diff --git a/Topers.Api/Program.cs b/Topers.Api/Program.cs index e4ea7c9..4766625 100644 --- a/Topers.Api/Program.cs +++ b/Topers.Api/Program.cs @@ -1,4 +1,4 @@ -using Topers.Api.Extenstions; +using Topers.Api.Extensions; var builder = WebApplication.CreateBuilder(args); { From 5b69fae9047cd8ba0d64e59c0d580a2adece7f80 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 28 Jul 2024 20:25:20 +0300 Subject: [PATCH 35/39] Fix OrderDetail Map --- Topers.Api/Controllers/OrdersController.cs | 65 ++++++++------ Topers.Api/Mapping/MappingProfile.cs | 89 +++++++++++++------ Topers.Core/Abstractions/IOrdersRepository.cs | 2 +- Topers.Core/Models/OrderDetails.cs | 7 +- .../Repositories/OrdersRepository.cs | 55 ++++++++---- .../Services/GoodsService.cs | 70 ++++++++++----- .../Services/OrdersService.cs | 52 +++++++---- 7 files changed, 222 insertions(+), 118 deletions(-) diff --git a/Topers.Api/Controllers/OrdersController.cs b/Topers.Api/Controllers/OrdersController.cs index 45fe049..0238b8c 100644 --- a/Topers.Api/Controllers/OrdersController.cs +++ b/Topers.Api/Controllers/OrdersController.cs @@ -8,16 +8,20 @@ namespace Topers.Api.Controllers { [ApiController] [Route("api/orders")] - public class OrdersController( - IOrdersService orderService - ) : ControllerBase + public class OrdersController(IOrdersService orderService) : ControllerBase { private readonly IOrdersService _ordersService = orderService; [HttpGet] - [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(IEnumerable))] + [SwaggerResponse( + 200, + Description = "Returns an orders list.", + Type = typeof(IEnumerable) + )] [SwaggerResponse(400, Description = "Orders not found.")] - public async Task>> GetOrders(CancellationToken cancellationToken) + public async Task>> GetOrders( + CancellationToken cancellationToken + ) { var orders = await _ordersService.GetAllOrdersAsync(cancellationToken); @@ -30,11 +34,16 @@ public async Task>> GetOrders(CancellationTo } [HttpGet("{orderId:guid}")] - [SwaggerResponse(200, Description = "Returns an orders list.", Type = typeof(OrderResponseDto))] + [SwaggerResponse( + 200, + Description = "Returns an orders list.", + Type = typeof(OrderResponseDto) + )] [SwaggerResponse(400, Description = "Orders not found.")] public async Task> GetOrderById( [FromRoute] Guid orderId, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { var order = await _ordersService.GetOrderByIdAsync(orderId, cancellationToken); @@ -51,15 +60,10 @@ public async Task> GetOrderById( [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> CreateGood( [FromBody] OrderRequestDto order, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { - var newOrder = new Order - ( - Guid.Empty, - order.Date, - order.CustomerId, - 0 - ); + var newOrder = new Order(Guid.Empty, order.Date, order.CustomerId, 0); var newOrderEntity = await _ordersService.CreateOrderAsync(newOrder, cancellationToken); @@ -70,12 +74,12 @@ public async Task> CreateGood( [SwaggerResponse(200, Description = "Update an existing order.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> UpdateOrder( - [FromRoute] Guid orderId, + [FromRoute] Guid orderId, [FromBody] UpdateOrderRequestDto order, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { - var existOrder = new Order - ( + var existOrder = new Order( orderId, DateTime.UtcNow, order.CustomerId, @@ -91,27 +95,32 @@ public async Task> UpdateOrder( [SwaggerResponse(200, Description = "Add good to an existing order.")] [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> AddProductToOrder( - [FromRoute] Guid orderId, + [FromRoute] Guid orderId, [FromBody] AddProductToOrderRequestDto orderDetail, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { - var newGoodDetail = new OrderDetails - ( + var newGoodDetail = new OrderDetails( + Guid.Empty, orderId, orderDetail.GoodScopeId, - orderDetail.GoodQuantity + orderDetail.GoodQuantity, + default ); - var newGoodScope = new GoodScope - ( + var newGoodScope = new GoodScope( Guid.Empty, orderDetail.GoodScopeId, orderDetail.GoodLitre ); - var newGoodDetailIdentifier = await _ordersService.AddGoodToOrderAsync(newGoodDetail, newGoodScope, cancellationToken); + var newGoodDetailIdentifier = await _ordersService.AddGoodToOrderAsync( + newGoodDetail, + newGoodScope, + cancellationToken + ); return Ok(newGoodDetailIdentifier); } } -} \ No newline at end of file +} diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index bcbeec2..aa05fc7 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -15,21 +15,29 @@ public MappingProfile() CreateMap(); CreateMap() - .ForMember(dest => dest.Address, opt => opt.MapFrom(src => src.Address != null ? new AddressResponseDto( - src.Address.Id, - src.Id, - src.Address.Street, - src.Address.City, - src.Address.State, - src.Address.PostalCode, - src.Address.Country - ) : null)); + .ForMember( + dest => dest.Address, + opt => + opt.MapFrom(src => + src.Address != null + ? new AddressResponseDto( + src.Address.Id, + src.Id, + src.Address.Street, + src.Address.City, + src.Address.State, + src.Address.PostalCode, + src.Address.Country + ) + : null + ) + ); CreateMap() - .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) - .ForMember(dest => dest.GoodId, opt => opt.MapFrom(src => src.GoodId)) - .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) - .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.GoodId, opt => opt.MapFrom(src => src.GoodId)) + .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) + .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) @@ -38,6 +46,13 @@ public MappingProfile() .ForMember(dest => dest.TotalPrice, opt => opt.MapFrom(src => src.TotalPrice)) .ForMember(dest => dest.OrderDetails, opt => opt.MapFrom(src => src.OrderDetails)); + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.OrderId, opt => opt.MapFrom(src => src.OrderId)) + .ForMember(dest => dest.GoodId, opt => opt.MapFrom(src => src.GoodId)) + .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) + .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) @@ -48,7 +63,6 @@ public MappingProfile() CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)); - CreateMap(); CreateMap(); @@ -58,27 +72,46 @@ public MappingProfile() CreateMap(); CreateMap() - .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + .ForMember( + dest => dest.Scopes, + opt => + opt.MapFrom(src => + src.Scopes != null ? src.Scopes : new List() + ) + ); CreateMap() - .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + .ForMember( + dest => dest.Scopes, + opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List()) + ); CreateMap() .ForMember(dest => dest.ImageName, opt => opt.MapFrom(src => src.ImageName)); CreateMap() - .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List())); + .ForMember( + dest => dest.Scopes, + opt => opt.MapFrom(src => src.Scopes != null ? src.Scopes : new List()) + ); CreateMap() - .ForMember(dest => dest.Scopes, opt => opt.MapFrom(src => - src.Scopes != null - ? src.Scopes.Select(scope => new GoodScopeResponseDto( - scope.Id, - scope.GoodId, - scope.Litre, - scope.Price, - scope.ImageName - )).ToList() - : new List())); + .ForMember( + dest => dest.Scopes, + opt => + opt.MapFrom(src => + src.Scopes != null + ? src + .Scopes.Select(scope => new GoodScopeResponseDto( + scope.Id, + scope.GoodId, + scope.Litre, + scope.Price, + scope.ImageName + )) + .ToList() + : new List() + ) + ); CreateMap(); CreateMap(); @@ -94,4 +127,4 @@ public MappingProfile() CreateMap(); CreateMap(); } -} \ No newline at end of file +} diff --git a/Topers.Core/Abstractions/IOrdersRepository.cs b/Topers.Core/Abstractions/IOrdersRepository.cs index 9da6def..193937c 100644 --- a/Topers.Core/Abstractions/IOrdersRepository.cs +++ b/Topers.Core/Abstractions/IOrdersRepository.cs @@ -10,4 +10,4 @@ public interface IOrdersRepository Task UpdateAsync(Order order, CancellationToken cancellationToken = default); Task DeleteAsync(Guid orderId, CancellationToken cancellationToken = default); Task AddDetailAsync(OrderDetails detail, CancellationToken cancellationToken = default); -}; \ No newline at end of file +}; diff --git a/Topers.Core/Models/OrderDetails.cs b/Topers.Core/Models/OrderDetails.cs index cdf9ff6..2038d80 100644 --- a/Topers.Core/Models/OrderDetails.cs +++ b/Topers.Core/Models/OrderDetails.cs @@ -5,12 +5,13 @@ namespace Topers.Core.Models; /// public class OrderDetails { - public OrderDetails(Guid orderId, Guid goodId, int quantity, decimal totalPrice = 0) + public OrderDetails(Guid id, Guid orderId, Guid goodId, int quantity, decimal price) { + Id = id; OrderId = orderId; GoodId = goodId; Quantity = quantity; - Price = totalPrice; + Price = price; } /// @@ -37,4 +38,4 @@ public OrderDetails(Guid orderId, Guid goodId, int quantity, decimal totalPrice /// Gets or sets a good price. /// public decimal Price { get; } = 0; -} \ No newline at end of file +} diff --git a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs index 4f32c77..c75c7cc 100644 --- a/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs +++ b/Topers.DataAccess.Postgres/Repositories/OrdersRepository.cs @@ -35,29 +35,30 @@ public async Task CreateAsync(Order order, CancellationToken cancellationT public async Task DeleteAsync(Guid orderId, CancellationToken cancellationToken = default) { - await _context.Orders - .Where(o => o.Id == orderId) - .ExecuteDeleteAsync(); + await _context.Orders.Where(o => o.Id == orderId).ExecuteDeleteAsync(); return orderId; } public async Task> GetAllAsync(CancellationToken cancellationToken = default) { - var orderEntities = await _context.Orders - .Include(o => o.OrderDetails) - .AsNoTracking() - .ToListAsync(); - + var orderEntities = await _context + .Orders.Include(o => o.OrderDetails) + .AsNoTracking() + .ToListAsync(); + var orders = _mapper.Map>(orderEntities); return orders; } - public async Task GetByIdAsync(Guid orderId, CancellationToken cancellationToken = default) + public async Task GetByIdAsync( + Guid orderId, + CancellationToken cancellationToken = default + ) { - var orderEntity = await _context.Orders - .Include(o => o.OrderDetails) + var orderEntity = await _context + .Orders.Include(o => o.OrderDetails) .AsNoTracking() .FirstOrDefaultAsync(o => o.Id == orderId); @@ -68,15 +69,19 @@ public async Task GetByIdAsync(Guid orderId, CancellationToken cancellati public async Task UpdateAsync(Order order, CancellationToken cancellationToken = default) { - await _context.Orders - .Where(o => o.Id == order.Id) - .ExecuteUpdateAsync(oUpdate => oUpdate - .SetProperty(o => o.CustomerId, order.CustomerId)); - + await _context + .Orders.Where(o => o.Id == order.Id) + .ExecuteUpdateAsync(oUpdate => + oUpdate.SetProperty(o => o.CustomerId, order.CustomerId) + ); + return order.Id; } - public async Task AddDetailAsync(OrderDetails detail, CancellationToken cancellationToken = default) + public async Task AddDetailAsync( + OrderDetails detail, + CancellationToken cancellationToken = default + ) { var orderDetailEntity = new OrderDetailsEntity { @@ -90,6 +95,20 @@ public async Task AddDetailAsync(OrderDetails detail, CancellationToken ca await _context.OrderDetails.AddAsync(orderDetailEntity); await _context.SaveChangesAsync(); + var orderEntity = await GetByIdAsync(orderDetailEntity.OrderId, cancellationToken); + + await _context + .Orders.Where(o => o.Id == orderDetailEntity.OrderId) + .ExecuteUpdateAsync( + oUpdate => + oUpdate.SetProperty( + o => o.TotalPrice, + orderEntity.TotalPrice + + (orderDetailEntity.Price * orderDetailEntity.Quantity) + ), + cancellationToken + ); + return orderDetailEntity.Id; } -} \ No newline at end of file +} diff --git a/Topers.Infrastructure/Services/GoodsService.cs b/Topers.Infrastructure/Services/GoodsService.cs index 559e0ad..1c79b8d 100644 --- a/Topers.Infrastructure/Services/GoodsService.cs +++ b/Topers.Infrastructure/Services/GoodsService.cs @@ -16,55 +16,77 @@ public GoodsService(IGoodsRepository goodsRepository, IMapper mapper) _mapper = mapper; } - public async Task CreateGoodAsync(Good good, CancellationToken cancellationToken = default) + public async Task CreateGoodAsync( + Good good, + CancellationToken cancellationToken = default + ) { - var newGoodIdentifier = await _goodsRepository.CreateAsync(good); + var newGoodIdentifier = await _goodsRepository.CreateAsync(good, cancellationToken); - var newGood = new GoodResponseDto - ( - newGoodIdentifier, - good.Name, - good.Description, - null - ); + var newGood = new GoodResponseDto(newGoodIdentifier, good.Name, good.Description, null); return newGood; } - public async Task DeleteGoodAsync(Guid goodId, CancellationToken cancellationToken = default) + public async Task DeleteGoodAsync( + Guid goodId, + CancellationToken cancellationToken = default + ) { - return await _goodsRepository.DeleteAsync(goodId); + return await _goodsRepository.DeleteAsync(goodId, cancellationToken); } - public async Task> GetAllGoodsAsync(CancellationToken cancellationToken = default) + public async Task> GetAllGoodsAsync( + CancellationToken cancellationToken = default + ) { - return _mapper.Map>(await _goodsRepository.GetAllAsync()); + return _mapper.Map>( + await _goodsRepository.GetAllAsync(cancellationToken) + ); } - public async Task GetGoodByIdAsync(Guid goodId, CancellationToken cancellationToken = default) + public async Task GetGoodByIdAsync( + Guid goodId, + CancellationToken cancellationToken = default + ) { - return _mapper.Map(await _goodsRepository.GetByIdAsync(goodId)); + return _mapper.Map( + await _goodsRepository.GetByIdAsync(goodId, cancellationToken) + ); } - public async Task UpdateGoodAsync(Good good, CancellationToken cancellationToken = default) + public async Task UpdateGoodAsync( + Good good, + CancellationToken cancellationToken = default + ) { - return await _goodsRepository.UpdateAsync(good); + return await _goodsRepository.UpdateAsync(good, cancellationToken); } - public async Task AddGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default) + public async Task AddGoodScopeAsync( + GoodScope scope, + CancellationToken cancellationToken = default + ) { - return await _goodsRepository.AddScopeAsync(scope); + return await _goodsRepository.AddScopeAsync(scope, cancellationToken); } - public async Task UpdateGoodScopeAsync(GoodScope scope, CancellationToken cancellationToken = default) + public async Task UpdateGoodScopeAsync( + GoodScope scope, + CancellationToken cancellationToken = default + ) { - return await _goodsRepository.UpdateScopeAsync(scope); + return await _goodsRepository.UpdateScopeAsync(scope, cancellationToken); } - public async Task IsGoodScopeExistsAsync(Guid goodId, int litre, CancellationToken cancellationToken = default) + public async Task IsGoodScopeExistsAsync( + Guid goodId, + int litre, + CancellationToken cancellationToken = default + ) { - var existingScope = await _goodsRepository.GetScopeAsync(goodId, litre); + var existingScope = await _goodsRepository.GetScopeAsync(goodId, litre, cancellationToken); return existingScope != null; } -} \ No newline at end of file +} diff --git a/Topers.Infrastructure/Services/OrdersService.cs b/Topers.Infrastructure/Services/OrdersService.cs index 229dadf..783858d 100644 --- a/Topers.Infrastructure/Services/OrdersService.cs +++ b/Topers.Infrastructure/Services/OrdersService.cs @@ -14,19 +14,22 @@ public class OrdersService : IOrdersService public OrdersService( IOrdersRepository ordersRepository, IGoodsRepository goodsRepository, - IMapper mapper) + IMapper mapper + ) { _ordersRepository = ordersRepository; _goodsRepository = goodsRepository; _mapper = mapper; } - public async Task CreateOrderAsync(Order order, CancellationToken cancellationToken = default) + public async Task CreateOrderAsync( + Order order, + CancellationToken cancellationToken = default + ) { - var newOrderIdentifier = await _ordersRepository.CreateAsync(order); + var newOrderIdentifier = await _ordersRepository.CreateAsync(order, cancellationToken); - var newOrder = new OrderResponseDto - ( + var newOrder = new OrderResponseDto( newOrderIdentifier, order.Date, order.CustomerId, @@ -36,38 +39,55 @@ public async Task CreateOrderAsync(Order order, CancellationTo return newOrder; } - public async Task DeleteOrderAsync(Guid orderId, CancellationToken cancellationToken = default) + public async Task DeleteOrderAsync( + Guid orderId, + CancellationToken cancellationToken = default + ) { - return await _ordersRepository.DeleteAsync(orderId); + return await _ordersRepository.DeleteAsync(orderId, cancellationToken); } - public async Task> GetAllOrdersAsync(CancellationToken cancellationToken = default) + public async Task> GetAllOrdersAsync( + CancellationToken cancellationToken = default + ) { return _mapper.Map>(await _ordersRepository.GetAllAsync()); } - public async Task GetOrderByIdAsync(Guid orderId, CancellationToken cancellationToken = default) + public async Task GetOrderByIdAsync( + Guid orderId, + CancellationToken cancellationToken = default + ) { - return _mapper.Map(await _ordersRepository.GetByIdAsync(orderId)); + return _mapper.Map( + await _ordersRepository.GetByIdAsync(orderId, cancellationToken) + ); } - public async Task UpdateOrderAsync(Order order, CancellationToken cancellationToken = default) + public async Task UpdateOrderAsync( + Order order, + CancellationToken cancellationToken = default + ) { return await _ordersRepository.UpdateAsync(order); } - public async Task AddGoodToOrderAsync(OrderDetails detail, GoodScope good, CancellationToken cancellationToken = default) + public async Task AddGoodToOrderAsync( + OrderDetails detail, + GoodScope good, + CancellationToken cancellationToken = default + ) { var goodScope = await _goodsRepository.GetScopeAsync(detail.GoodId, good.Litre); - var newGoodDetailEntity = new OrderDetails - ( + var newGoodDetailEntity = new OrderDetails( + Guid.Empty, detail.OrderId, goodScope.Id, detail.Quantity, goodScope.Price ); - return await _ordersRepository.AddDetailAsync(newGoodDetailEntity); + return await _ordersRepository.AddDetailAsync(newGoodDetailEntity, cancellationToken); } -} \ No newline at end of file +} From ea87276f9cba610f5d31fcba3153366746266cc7 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 28 Jul 2024 20:49:12 +0300 Subject: [PATCH 36/39] Implement CartEntity & CartItemEntity --- .../Configurations/CartConfiguration.cs | 21 + .../Configurations/CartItemConfiguration.cs | 17 + .../Entities/CartEntity.cs | 37 ++ .../Entities/CartItemEntity.cs | 42 ++ .../Entities/CustomerEntity.cs | 7 +- .../Entities/GoodScopeEntity.cs | 3 +- .../20240728174427_AddCart.Designer.cs | 407 ++++++++++++++++++ .../Migrations/20240728174427_AddCart.cs | 88 ++++ .../TopersDbContextModelSnapshot.cs | 89 ++++ Topers.DataAccess.Postgres/TopersDbContext.cs | 4 +- 10 files changed, 712 insertions(+), 3 deletions(-) create mode 100644 Topers.DataAccess.Postgres/Configurations/CartConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Configurations/CartItemConfiguration.cs create mode 100644 Topers.DataAccess.Postgres/Entities/CartEntity.cs create mode 100644 Topers.DataAccess.Postgres/Entities/CartItemEntity.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.Designer.cs create mode 100644 Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.cs diff --git a/Topers.DataAccess.Postgres/Configurations/CartConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/CartConfiguration.cs new file mode 100644 index 0000000..6d8aead --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/CartConfiguration.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class CartConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(c => c.Id); + builder + .HasOne(c => c.Customer) + .WithOne(o => o.Cart) + .HasForeignKey(c => c.CustomerId); + builder + .HasMany(c => c.CartItems) + .WithOne(i => i.Cart) + .HasForeignKey(i => i.CartId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Configurations/CartItemConfiguration.cs b/Topers.DataAccess.Postgres/Configurations/CartItemConfiguration.cs new file mode 100644 index 0000000..74640ff --- /dev/null +++ b/Topers.DataAccess.Postgres/Configurations/CartItemConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Configurations; + +public class CartItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(i => i.Id); + builder + .HasOne(i => i.Good) + .WithMany(g => g.CartDetails) + .HasForeignKey(i => i.GoodId); + } +} \ No newline at end of file diff --git a/Topers.DataAccess.Postgres/Entities/CartEntity.cs b/Topers.DataAccess.Postgres/Entities/CartEntity.cs new file mode 100644 index 0000000..9c0ae84 --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/CartEntity.cs @@ -0,0 +1,37 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents a shopping cart. +/// +public class CartEntity +{ + /// + /// Gets or sets the cart identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the customer identifier. + /// + public Guid CustomerId { get; set; } + + /// + /// ets or sets the customer. + /// + public CustomerEntity Customer { get; set; } = null!; + + /// + /// Gets or sets the date when the cart was created. + /// + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the date when the cart was last updated. + /// + public DateTime UpdatedDate { get; set; } = DateTime.UtcNow; + + /// + /// Gets or sets the cart items. + /// + public ICollection CartItems { get; set; } = []; +} diff --git a/Topers.DataAccess.Postgres/Entities/CartItemEntity.cs b/Topers.DataAccess.Postgres/Entities/CartItemEntity.cs new file mode 100644 index 0000000..3a08afe --- /dev/null +++ b/Topers.DataAccess.Postgres/Entities/CartItemEntity.cs @@ -0,0 +1,42 @@ +namespace Topers.DataAccess.Postgres.Entities; + +/// +/// Represents an item in the shopping cart. +/// +public class CartItemEntity +{ + /// + /// Gets or sets the cart item identifier. + /// + public Guid Id { get; set; } + + /// + /// Gets or sets the cart identifier. + /// + public Guid CartId { get; set; } + + /// + /// Gets or sets the cart. + /// + public CartEntity Cart { get; set; } = null!; + + /// + /// Gets or sets the good identifier. + /// + public Guid GoodId { get; set; } + + /// + /// Gets or sets the good. + /// + public GoodScopeEntity Good { get; set; } = null!; + + /// + /// Gets or sets the quantity of the product. + /// + public int Quantity { get; set; } + + /// + /// Gets or sets the price of the product. + /// + public decimal Price { get; set; } +} diff --git a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs index 401a749..eebd2b4 100644 --- a/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/CustomerEntity.cs @@ -30,8 +30,13 @@ public class CustomerEntity /// public AddressEntity? Address { get; set; } + /// + /// Gets or sets a customer cart. + /// + public CartEntity? Cart { get; set; } + /// /// Gets or sets a customer orders. /// public ICollection Orders { get; set; } = []; -} \ No newline at end of file +} diff --git a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs index 3f45a5d..b99950b 100644 --- a/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs +++ b/Topers.DataAccess.Postgres/Entities/GoodScopeEntity.cs @@ -48,4 +48,5 @@ public class GoodScopeEntity /// Gets or sets an order details about good. /// public ICollection? OrderDetails { get; set; } = []; -} \ No newline at end of file + public ICollection? CartDetails { get; set; } = []; +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.Designer.cs b/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.Designer.cs new file mode 100644 index 0000000..dbb002e --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.Designer.cs @@ -0,0 +1,407 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Topers.DataAccess.Postgres; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + [DbContext(typeof(TopersDbContext))] + [Migration("20240728174427_AddCart")] + partial class AddCart + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.6") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Country") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .IsRequired() + .HasColumnType("text"); + + b.Property("Street") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Addresses"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Cart"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.HasIndex("GoodId"); + + b.ToTable("CartDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CategoryId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Image") + .HasColumnType("text"); + + b.Property("Litre") + .HasColumnType("integer"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.ToTable("GoodScopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("OrderId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GoodId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("TotalPrice") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.AddressEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Address") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.AddressEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Cart") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.CartEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartItemEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CartEntity", "Cart") + .WithMany("CartItems") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", "Good") + .WithMany("CartDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") + .WithMany("Goods") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodEntity", "Good") + .WithMany("Scopes") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderDetailsEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", "Good") + .WithMany("OrderDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.OrderEntity", "Order") + .WithMany("OrderDetails") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Good"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.Navigation("CartItems"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => + { + b.Navigation("Goods"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CustomerEntity", b => + { + b.Navigation("Address"); + + b.Navigation("Cart"); + + b.Navigation("Orders"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => + { + b.Navigation("Scopes"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => + { + b.Navigation("CartDetails"); + + b.Navigation("OrderDetails"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.OrderEntity", b => + { + b.Navigation("OrderDetails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.cs b/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.cs new file mode 100644 index 0000000..b0e4f50 --- /dev/null +++ b/Topers.DataAccess.Postgres/Migrations/20240728174427_AddCart.cs @@ -0,0 +1,88 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Topers.DataAccess.Postgres.Migrations +{ + /// + public partial class AddCart : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Cart", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CustomerId = table.Column(type: "uuid", nullable: false), + CreatedDate = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Cart", x => x.Id); + table.ForeignKey( + name: "FK_Cart_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CartDetails", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + CartId = table.Column(type: "uuid", nullable: false), + GoodId = table.Column(type: "uuid", nullable: false), + Quantity = table.Column(type: "integer", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CartDetails", x => x.Id); + table.ForeignKey( + name: "FK_CartDetails_Cart_CartId", + column: x => x.CartId, + principalTable: "Cart", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CartDetails_GoodScopes_GoodId", + column: x => x.GoodId, + principalTable: "GoodScopes", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Cart_CustomerId", + table: "Cart", + column: "CustomerId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CartDetails_CartId", + table: "CartDetails", + column: "CartId"); + + migrationBuilder.CreateIndex( + name: "IX_CartDetails_GoodId", + table: "CartDetails", + column: "GoodId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CartDetails"); + + migrationBuilder.DropTable( + name: "Cart"); + } + } +} diff --git a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs index 3881ab8..df7c780 100644 --- a/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs +++ b/Topers.DataAccess.Postgres/Migrations/TopersDbContextModelSnapshot.cs @@ -59,6 +59,56 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Addresses"); }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomerId") + .HasColumnType("uuid"); + + b.Property("UpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId") + .IsUnique(); + + b.ToTable("Cart"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CartId") + .HasColumnType("uuid"); + + b.Property("GoodId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CartId"); + + b.HasIndex("GoodId"); + + b.ToTable("CartDetails"); + }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => { b.Property("Id") @@ -231,6 +281,36 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Customer"); }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CustomerEntity", "Customer") + .WithOne("Cart") + .HasForeignKey("Topers.DataAccess.Postgres.Entities.CartEntity", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartItemEntity", b => + { + b.HasOne("Topers.DataAccess.Postgres.Entities.CartEntity", "Cart") + .WithMany("CartItems") + .HasForeignKey("CartId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", "Good") + .WithMany("CartDetails") + .HasForeignKey("GoodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cart"); + + b.Navigation("Good"); + }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodEntity", b => { b.HasOne("Topers.DataAccess.Postgres.Entities.CategoryEntity", "Category") @@ -283,6 +363,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Customer"); }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CartEntity", b => + { + b.Navigation("CartItems"); + }); + modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.CategoryEntity", b => { b.Navigation("Goods"); @@ -292,6 +377,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("Address"); + b.Navigation("Cart"); + b.Navigation("Orders"); }); @@ -302,6 +389,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Topers.DataAccess.Postgres.Entities.GoodScopeEntity", b => { + b.Navigation("CartDetails"); + b.Navigation("OrderDetails"); }); diff --git a/Topers.DataAccess.Postgres/TopersDbContext.cs b/Topers.DataAccess.Postgres/TopersDbContext.cs index 80a6c77..5f90cef 100644 --- a/Topers.DataAccess.Postgres/TopersDbContext.cs +++ b/Topers.DataAccess.Postgres/TopersDbContext.cs @@ -14,6 +14,8 @@ public class TopersDbContext(DbContextOptions options) : DbCont public DbSet Customers { get; set; } public DbSet Orders { get; set; } public DbSet OrderDetails { get; set; } + public DbSet Cart { get; set; } + public DbSet CartDetails { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -27,4 +29,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) base.OnModelCreating(modelBuilder); } -} \ No newline at end of file +} From 5eec81a1b30c7c0fb0e10129853c50d44842839b Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 28 Jul 2024 21:27:54 +0300 Subject: [PATCH 37/39] Implement CartsRepository & CartsService. Add model --- Topers.Core/Abstractions/ICartService.cs | 13 +++ Topers.Core/Abstractions/ICartsRepository.cs | 12 +++ Topers.Core/Dtos/CartDto.cs | 13 +++ Topers.Core/Dtos/CartItemDto.cs | 16 ++++ Topers.Core/Models/Cart.cs | 35 ++++++++ Topers.Core/Models/CartItems.cs | 41 +++++++++ .../Repositories/CartsRepository.cs | 89 +++++++++++++++++++ .../Services/CartsService.cs | 84 +++++++++++++++++ 8 files changed, 303 insertions(+) create mode 100644 Topers.Core/Abstractions/ICartService.cs create mode 100644 Topers.Core/Abstractions/ICartsRepository.cs create mode 100644 Topers.Core/Dtos/CartDto.cs create mode 100644 Topers.Core/Dtos/CartItemDto.cs create mode 100644 Topers.Core/Models/Cart.cs create mode 100644 Topers.Core/Models/CartItems.cs create mode 100644 Topers.DataAccess.Postgres/Repositories/CartsRepository.cs create mode 100644 Topers.Infrastructure/Services/CartsService.cs diff --git a/Topers.Core/Abstractions/ICartService.cs b/Topers.Core/Abstractions/ICartService.cs new file mode 100644 index 0000000..2b149b4 --- /dev/null +++ b/Topers.Core/Abstractions/ICartService.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Dtos; +using Topers.Core.Models; + +public interface ICartsService +{ + Task GetCartById(Guid cartId, CancellationToken cancellationToken = default); + Task GetCartByCustomerId(Guid customerId, CancellationToken cancellationToken = default); + Task CreateCartAsync(Cart cart, CancellationToken cancellationToken = default); + Task DeleteCartAsync(Guid cartId, CancellationToken cancellationToken = default); + Task AddGoodToCartAsync(CartItems cartDetails, GoodScope good, CancellationToken cancellationToken = default); +}; \ No newline at end of file diff --git a/Topers.Core/Abstractions/ICartsRepository.cs b/Topers.Core/Abstractions/ICartsRepository.cs new file mode 100644 index 0000000..b508431 --- /dev/null +++ b/Topers.Core/Abstractions/ICartsRepository.cs @@ -0,0 +1,12 @@ +namespace Topers.Core.Abstractions; + +using Topers.Core.Models; + +public interface ICartsRepository +{ + Task GetById(Guid cartId, CancellationToken cancellationToken = default); + Task GetByCustomerId(Guid customerId, CancellationToken cancellationToken = default); + Task CreateAsync(Cart cart, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid cartId, CancellationToken cancellationToken = default); + Task AddDetailAsync(CartItems cartDetails, CancellationToken cancellationToken = default); +}; diff --git a/Topers.Core/Dtos/CartDto.cs b/Topers.Core/Dtos/CartDto.cs new file mode 100644 index 0000000..4e48676 --- /dev/null +++ b/Topers.Core/Dtos/CartDto.cs @@ -0,0 +1,13 @@ +namespace Topers.Core.Dtos; + +public record CartResponseDto( + Guid Id, + Guid CustomerId, + DateTime CreatedDate, + DateTime UpdatedDate, + List? OrderDetails = null +); + +public record CartRequestDto( + Guid CustomerId +); diff --git a/Topers.Core/Dtos/CartItemDto.cs b/Topers.Core/Dtos/CartItemDto.cs new file mode 100644 index 0000000..bb8e55c --- /dev/null +++ b/Topers.Core/Dtos/CartItemDto.cs @@ -0,0 +1,16 @@ +namespace Topers.Core.Dtos; + +public record CartItemResponseDto( + Guid Id, + Guid CartId, + Guid GoodId, + int Quantity, + decimal Price +); + +public record CartItemRequestDto( + Guid CartId, + Guid GoodId, + int Quantity, + decimal Price +); \ No newline at end of file diff --git a/Topers.Core/Models/Cart.cs b/Topers.Core/Models/Cart.cs new file mode 100644 index 0000000..f097e9c --- /dev/null +++ b/Topers.Core/Models/Cart.cs @@ -0,0 +1,35 @@ +namespace Topers.Core.Models; + +/// +/// Represents a customer cart. +/// +public class Cart +{ + public Cart(Guid id, Guid customerId, DateTime createdDate, DateTime updatedDate) + { + Id = id; + CustomerId = customerId; + CreatedDate = createdDate; + UpdatedDate = updatedDate; + } + + /// + /// Gets a cart identifier. + /// + public Guid Id { get; } + + /// + /// Gets a customer identifier. + /// + public Guid CustomerId { get; } + + /// + /// Gets a cart created date. + /// + public DateTime CreatedDate { get; } + + /// + /// Gets a cart updated date. + /// + public DateTime UpdatedDate { get; } +} diff --git a/Topers.Core/Models/CartItems.cs b/Topers.Core/Models/CartItems.cs new file mode 100644 index 0000000..f668a94 --- /dev/null +++ b/Topers.Core/Models/CartItems.cs @@ -0,0 +1,41 @@ +namespace Topers.Core.Models; + +/// +/// Represents a customer cart. +/// +public class CartItems +{ + public CartItems(Guid id, Guid cartId, Guid goodId, int quantity, decimal price) + { + Id = id; + CartId = cartId; + GoodId = goodId; + Quantity = quantity; + Price = price; + } + + /// + /// Gets a cart item identifier. + /// + public Guid Id { get; } + + /// + /// Gets a cart identifier. + /// + public Guid CartId { get; } + + /// + /// Gets a cart good scope identifier. + /// + public Guid GoodId { get; } + + /// + /// Gets a good quantity. + /// + public int Quantity { get; } = 0; + + /// + /// Gets a good price. + /// + public decimal Price { get; } = 0; +} diff --git a/Topers.DataAccess.Postgres/Repositories/CartsRepository.cs b/Topers.DataAccess.Postgres/Repositories/CartsRepository.cs new file mode 100644 index 0000000..592c472 --- /dev/null +++ b/Topers.DataAccess.Postgres/Repositories/CartsRepository.cs @@ -0,0 +1,89 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Topers.Core.Abstractions; +using Topers.Core.Models; +using Topers.DataAccess.Postgres.Entities; + +namespace Topers.DataAccess.Postgres.Repositories; + +public class CartsRepository : ICartsRepository +{ + private readonly TopersDbContext _context; + private readonly IMapper _mapper; + + public CartsRepository(TopersDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task AddDetailAsync( + CartItems cartDetails, + CancellationToken cancellationToken = default + ) + { + var cartItemEntity = new CartItemEntity + { + Id = Guid.NewGuid(), + CartId = cartDetails.CartId, + GoodId = cartDetails.GoodId, + Quantity = cartDetails.Quantity, + Price = cartDetails.Price + }; + + await _context.CartDetails.AddAsync(cartItemEntity); + await _context.SaveChangesAsync(); + + return cartItemEntity.Id; + } + + public async Task CreateAsync(Cart cart, CancellationToken cancellationToken = default) + { + var cartEntity = new CartEntity + { + Id = Guid.NewGuid(), + CustomerId = cart.CustomerId, + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow + }; + + await _context.Cart.AddAsync(cartEntity); + await _context.SaveChangesAsync(); + + return cartEntity.Id; + } + + public async Task DeleteAsync(Guid cartId, CancellationToken cancellationToken = default) + { + await _context.Cart.Where(c => c.Id == cartId).ExecuteDeleteAsync(); + + return cartId; + } + + public async Task GetByCustomerId( + Guid customerId, + CancellationToken cancellationToken = default + ) + { + var cartEntity = await _context + .Cart.Where(c => c.CustomerId == customerId) + .AsNoTracking() + .ToListAsync(); + + var cart = _mapper.Map(cartEntity); + + return cart; + } + + public async Task GetById(Guid cartId, CancellationToken cancellationToken = default) + { + var cartEntity = await _context + .Cart.Where(c => c.Id == cartId) + .AsNoTracking() + .ToListAsync(); + + var cart = _mapper.Map(cartEntity); + + return cart; + } +} diff --git a/Topers.Infrastructure/Services/CartsService.cs b/Topers.Infrastructure/Services/CartsService.cs new file mode 100644 index 0000000..4995435 --- /dev/null +++ b/Topers.Infrastructure/Services/CartsService.cs @@ -0,0 +1,84 @@ +using AutoMapper; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Infrastructure.Services; + +public class CartsService : ICartsService +{ + private readonly ICartsRepository _cartsRepository; + private readonly IGoodsRepository _goodsRepository; + private readonly IMapper _mapper; + + public CartsService(ICartsRepository cartsRepository, IGoodsRepository goodsRepository, IMapper mapper) + { + _cartsRepository = cartsRepository; + _goodsRepository = goodsRepository; + _mapper = mapper; + } + + public async Task AddGoodToCartAsync( + CartItems cartDetails, + GoodScope good, + CancellationToken cancellationToken = default + ) + { + var goodScope = await _goodsRepository.GetScopeAsync(cartDetails.GoodId, good.Litre); + + var newCartItemsEntity = new CartItems( + Guid.Empty, + cartDetails.CartId, + goodScope.GoodId, + cartDetails.Quantity, + goodScope.Price + ); + + return await _cartsRepository.AddDetailAsync(newCartItemsEntity, cancellationToken); + } + + public async Task CreateCartAsync( + Cart cart, + CancellationToken cancellationToken = default + ) + { + var newCartIdentifier = await _cartsRepository.CreateAsync(cart, cancellationToken); + + var newCart = new CartResponseDto( + newCartIdentifier, + cart.CustomerId, + cart.CreatedDate, + cart.UpdatedDate + ); + + return newCart; + } + + public async Task DeleteCartAsync( + Guid cartId, + CancellationToken cancellationToken = default + ) + { + return await _cartsRepository.DeleteAsync(cartId); + } + + public async Task GetCartByCustomerId( + Guid customerId, + CancellationToken cancellationToken = default + ) + { + return _mapper.Map( + await _cartsRepository.GetByCustomerId(customerId, cancellationToken) + ); + } + + public async Task GetCartById( + Guid cartId, + CancellationToken cancellationToken = default + ) + { + return _mapper.Map( + await _cartsRepository.GetById(cartId, cancellationToken) + ); + } +} From d2478e81bddf19ce0927ea4899c062a48a3face4 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 28 Jul 2024 21:36:37 +0300 Subject: [PATCH 38/39] Set up the Mapper settings. Add scopes --- Topers.Api/Extensions/ServiceExtensions.cs | 2 ++ Topers.Api/Mapping/MappingProfile.cs | 19 ++++++++++++++++++- Topers.Core/Dtos/CartDto.cs | 2 +- Topers.Core/Models/Cart.cs | 5 +++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Topers.Api/Extensions/ServiceExtensions.cs b/Topers.Api/Extensions/ServiceExtensions.cs index 73ea0dd..0997672 100644 --- a/Topers.Api/Extensions/ServiceExtensions.cs +++ b/Topers.Api/Extensions/ServiceExtensions.cs @@ -27,6 +27,7 @@ public static IServiceCollection AddCustomServices(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -34,6 +35,7 @@ public static IServiceCollection AddCustomServices(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Topers.Api/Mapping/MappingProfile.cs b/Topers.Api/Mapping/MappingProfile.cs index aa05fc7..54d85bf 100644 --- a/Topers.Api/Mapping/MappingProfile.cs +++ b/Topers.Api/Mapping/MappingProfile.cs @@ -33,12 +33,29 @@ public MappingProfile() ) ); - CreateMap() + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.CartId, opt => opt.MapFrom(src => src.CartId)) .ForMember(dest => dest.GoodId, opt => opt.MapFrom(src => src.GoodId)) .ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quantity)) .ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price)); + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.CreatedDate, opt => opt.MapFrom(src => src.CreatedDate)) + .ForMember(dest => dest.UpdatedDate, opt => opt.MapFrom(src => src.UpdatedDate)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.CustomerId)) + .ForMember(dest => dest.CreatedDate, opt => opt.MapFrom(src => src.CreatedDate)) + .ForMember(dest => dest.UpdatedDate, opt => opt.MapFrom(src => src.UpdatedDate)) + .ForMember(dest => dest.CartDetails, opt => opt.MapFrom(src => src.CartDetails)); + + CreateMap() + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)); + CreateMap() .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id)) .ForMember(dest => dest.Date, opt => opt.MapFrom(src => src.Date)) diff --git a/Topers.Core/Dtos/CartDto.cs b/Topers.Core/Dtos/CartDto.cs index 4e48676..a1442ce 100644 --- a/Topers.Core/Dtos/CartDto.cs +++ b/Topers.Core/Dtos/CartDto.cs @@ -5,7 +5,7 @@ public record CartResponseDto( Guid CustomerId, DateTime CreatedDate, DateTime UpdatedDate, - List? OrderDetails = null + List? CartDetails = null ); public record CartRequestDto( diff --git a/Topers.Core/Models/Cart.cs b/Topers.Core/Models/Cart.cs index f097e9c..754dea6 100644 --- a/Topers.Core/Models/Cart.cs +++ b/Topers.Core/Models/Cart.cs @@ -32,4 +32,9 @@ public Cart(Guid id, Guid customerId, DateTime createdDate, DateTime updatedDate /// Gets a cart updated date. /// public DateTime UpdatedDate { get; } + + /// + /// Gets or sets a good scopes. + /// + public ICollection CartDetails { get; set; } } From d1029a8f7ae1bdd2f03df39659f7d01821210506 Mon Sep 17 00:00:00 2001 From: Vanya Chernov Date: Sun, 28 Jul 2024 21:46:17 +0300 Subject: [PATCH 39/39] Implement CartsController --- Topers.Api/Controllers/CartsController.cs | 112 +++++++++++++++++++++ Topers.Api/Controllers/OrdersController.cs | 2 +- Topers.Core/Dtos/OrderDto.cs | 2 +- 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 Topers.Api/Controllers/CartsController.cs diff --git a/Topers.Api/Controllers/CartsController.cs b/Topers.Api/Controllers/CartsController.cs new file mode 100644 index 0000000..0f94621 --- /dev/null +++ b/Topers.Api/Controllers/CartsController.cs @@ -0,0 +1,112 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using Topers.Core.Abstractions; +using Topers.Core.Dtos; +using Topers.Core.Models; + +namespace Topers.Api.Controllers +{ + [ApiController] + [Route("api/cart")] + public class CartsController(ICartsService cartService) : ControllerBase + { + private readonly ICartsService _cartService = cartService; + + [HttpGet("{cartId:guid}")] + [SwaggerResponse( + 200, + Description = "Returns a cart by identifier.", + Type = typeof(CartResponseDto) + )] + [SwaggerResponse(400, Description = "Cart not found.")] + public async Task> GetCartById( + [FromRoute] Guid cartId, + CancellationToken cancellationToken + ) + { + var cart = await _cartService.GetCartById(cartId, cancellationToken); + + if (cart == null) + { + return NotFound(); + } + + return Ok(cart); + } + + [HttpGet("{customerId:guid}")] + [SwaggerResponse( + 200, + Description = "Returns a cart by customer identifier.", + Type = typeof(CartResponseDto) + )] + [SwaggerResponse(400, Description = "Cart not found.")] + public async Task> GetCartByCustomerId( + [FromRoute] Guid customerId, + CancellationToken cancellationToken + ) + { + var cart = await _cartService.GetCartByCustomerId(customerId, cancellationToken); + + if (cart == null) + { + return NotFound(); + } + + return Ok(cart); + } + + [HttpPost("create")] + [SwaggerResponse(200, Description = "Create a new cart.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] + public async Task> CreateCart( + [FromBody] CartRequestDto cart, + CancellationToken cancellationToken + ) + { + var newCart = new Cart + ( + Guid.Empty, + cart.CustomerId, + DateTime.UtcNow, + DateTime.UtcNow + ); + + var newCartEntity = await _cartService.CreateCartAsync(newCart, cancellationToken); + + return Ok(newCartEntity); + } + + [HttpPost("{cartId:guid}/addGood")] + [SwaggerResponse(200, Description = "Add good to a customer cart.")] + [SwaggerResponse(400, Description = "There are some errors in the model.")] + public async Task> AddProductToCart( + [FromRoute] Guid cartId, + [FromBody] AddProductRequestDto cartDetail, + CancellationToken cancellationToken + ) + { + var newGoodDetail = new CartItems( + Guid.Empty, + cartId, + cartDetail.GoodScopeId, + cartDetail.GoodQuantity, + default + ); + + var newGoodScope = new GoodScope( + Guid.Empty, + cartDetail.GoodScopeId, + cartDetail.GoodLitre + ); + + var newGoodDetailIdentifier = await _cartService.AddGoodToCartAsync( + newGoodDetail, + newGoodScope, + cancellationToken + ); + + return Ok(newGoodDetailIdentifier); + } + } +} diff --git a/Topers.Api/Controllers/OrdersController.cs b/Topers.Api/Controllers/OrdersController.cs index 0238b8c..47b8620 100644 --- a/Topers.Api/Controllers/OrdersController.cs +++ b/Topers.Api/Controllers/OrdersController.cs @@ -96,7 +96,7 @@ CancellationToken cancellationToken [SwaggerResponse(400, Description = "There are some errors in the model.")] public async Task> AddProductToOrder( [FromRoute] Guid orderId, - [FromBody] AddProductToOrderRequestDto orderDetail, + [FromBody] AddProductRequestDto orderDetail, CancellationToken cancellationToken ) { diff --git a/Topers.Core/Dtos/OrderDto.cs b/Topers.Core/Dtos/OrderDto.cs index 65f90e8..391c604 100644 --- a/Topers.Core/Dtos/OrderDto.cs +++ b/Topers.Core/Dtos/OrderDto.cs @@ -18,7 +18,7 @@ public record UpdateOrderRequestDto( Guid CustomerId ); -public record AddProductToOrderRequestDto( +public record AddProductRequestDto( Guid GoodScopeId, int GoodQuantity, int GoodLitre