define basic shopping cart behaviors

This commit is contained in:
2023-11-03 13:23:44 -05:00
parent 14d311b5f1
commit f1a744d200
22 changed files with 516 additions and 3 deletions

View File

@@ -0,0 +1,52 @@
using FakePieShop.Models;
using FakePieShop.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace FakePieShop.Controllers
{
public class ShoppingCartController : Controller
{
private readonly IPieRepository _pieRepository;
private readonly IShoppingCart _shoppingCart;
public ShoppingCartController(IPieRepository pieRepository, IShoppingCart shoppingCart)
{
_pieRepository = pieRepository;
_shoppingCart = shoppingCart;
}
public ViewResult Index()
{
var items = _shoppingCart.GetShoppingCartItems();
_shoppingCart.ShoppingCartItems = items;
var shoppingCartViewModel = new ShoppingCartViewModel(_shoppingCart, _shoppingCart.GetShoppingCartTotal());
return View(shoppingCartViewModel);
}
public RedirectToActionResult AddToShoppingCart(int pieId)
{
var selectedPie = _pieRepository.GetPieById(pieId);
if (selectedPie != null)
{
_shoppingCart.AddToCart(selectedPie);
}
return RedirectToAction("Index");
}
public RedirectToActionResult RemoveFromShoppingCart(int pieId)
{
var selectedPie = _pieRepository.GetPieById(pieId);
if (selectedPie != null)
{
_shoppingCart.RemoveFromCart(selectedPie);
}
return RedirectToAction("Index");
}
}
}

View File

@@ -0,0 +1,146 @@
// <auto-generated />
using FakePieShop.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FakePieShop.Migrations
{
[DbContext(typeof(FakePieShopDbContext))]
[Migration("20231103172053_AddShoppingCartItem")]
partial class AddShoppingCartItem
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.13")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FakePieShop.Models.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("CategoryId"));
b.Property<string>("CategoryName")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Description")
.HasColumnType("nvarchar(max)");
b.HasKey("CategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("FakePieShop.Models.Pie", b =>
{
b.Property<int>("PieId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("PieId"));
b.Property<string>("AllergyInformation")
.HasColumnType("nvarchar(max)");
b.Property<int>("CategoryId")
.HasColumnType("int");
b.Property<string>("ImageThumbnailUrl")
.HasColumnType("nvarchar(max)");
b.Property<string>("ImageUrl")
.HasColumnType("nvarchar(max)");
b.Property<bool>("InStock")
.HasColumnType("bit");
b.Property<bool>("IsPieOfTheWeek")
.HasColumnType("bit");
b.Property<string>("LongDescription")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<decimal>("Price")
.HasColumnType("decimal(18,2)");
b.Property<string>("ShortDescription")
.HasColumnType("nvarchar(max)");
b.HasKey("PieId");
b.HasIndex("CategoryId");
b.ToTable("Pies");
});
modelBuilder.Entity("FakePieShop.Models.ShoppingCartItem", b =>
{
b.Property<int>("ShoppingCartItemId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ShoppingCartItemId"));
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<int>("PieId")
.HasColumnType("int");
b.Property<string>("ShoppingCartId")
.HasColumnType("nvarchar(max)");
b.HasKey("ShoppingCartItemId");
b.HasIndex("PieId");
b.ToTable("ShoppingCartItems");
});
modelBuilder.Entity("FakePieShop.Models.Pie", b =>
{
b.HasOne("FakePieShop.Models.Category", "Category")
.WithMany("Pies")
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("FakePieShop.Models.ShoppingCartItem", b =>
{
b.HasOne("FakePieShop.Models.Pie", "Pie")
.WithMany()
.HasForeignKey("PieId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Pie");
});
modelBuilder.Entity("FakePieShop.Models.Category", b =>
{
b.Navigation("Pies");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,47 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FakePieShop.Migrations
{
/// <inheritdoc />
public partial class AddShoppingCartItem : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ShoppingCartItems",
columns: table => new
{
ShoppingCartItemId = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
PieId = table.Column<int>(type: "int", nullable: false),
Amount = table.Column<int>(type: "int", nullable: false),
ShoppingCartId = table.Column<string>(type: "nvarchar(max)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShoppingCartItems", x => x.ShoppingCartItemId);
table.ForeignKey(
name: "FK_ShoppingCartItems_Pies_PieId",
column: x => x.PieId,
principalTable: "Pies",
principalColumn: "PieId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ShoppingCartItems_PieId",
table: "ShoppingCartItems",
column: "PieId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ShoppingCartItems");
}
}
}

View File

@@ -87,6 +87,30 @@ namespace FakePieShop.Migrations
b.ToTable("Pies"); b.ToTable("Pies");
}); });
modelBuilder.Entity("FakePieShop.Models.ShoppingCartItem", b =>
{
b.Property<int>("ShoppingCartItemId")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ShoppingCartItemId"));
b.Property<int>("Amount")
.HasColumnType("int");
b.Property<int>("PieId")
.HasColumnType("int");
b.Property<string>("ShoppingCartId")
.HasColumnType("nvarchar(max)");
b.HasKey("ShoppingCartItemId");
b.HasIndex("PieId");
b.ToTable("ShoppingCartItems");
});
modelBuilder.Entity("FakePieShop.Models.Pie", b => modelBuilder.Entity("FakePieShop.Models.Pie", b =>
{ {
b.HasOne("FakePieShop.Models.Category", "Category") b.HasOne("FakePieShop.Models.Category", "Category")
@@ -98,6 +122,17 @@ namespace FakePieShop.Migrations
b.Navigation("Category"); b.Navigation("Category");
}); });
modelBuilder.Entity("FakePieShop.Models.ShoppingCartItem", b =>
{
b.HasOne("FakePieShop.Models.Pie", "Pie")
.WithMany()
.HasForeignKey("PieId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Pie");
});
modelBuilder.Entity("FakePieShop.Models.Category", b => modelBuilder.Entity("FakePieShop.Models.Category", b =>
{ {
b.Navigation("Pies"); b.Navigation("Pies");

View File

@@ -11,5 +11,6 @@ namespace FakePieShop.Models
public DbSet<Category> Categories { get; set; } public DbSet<Category> Categories { get; set; }
public DbSet<Pie> Pies { get; set; } public DbSet<Pie> Pies { get; set; }
public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }
} }
} }

View File

@@ -0,0 +1,12 @@
namespace FakePieShop.Models
{
public interface IShoppingCart
{
void AddToCart(Pie pie);
int RemoveFromCart(Pie pie);
List<ShoppingCartItem> GetShoppingCartItems();
void ClearCart();
decimal GetShoppingCartTotal();
List<ShoppingCartItem> ShoppingCartItems { get; set; }
}
}

View File

@@ -0,0 +1,116 @@
using Microsoft.EntityFrameworkCore;
namespace FakePieShop.Models
{
public class ShoppingCart : IShoppingCart
{
private readonly FakePieShopDbContext _fakePieShopDbContext;
public string? ShoppingCartId { get; set; }
public List<ShoppingCartItem>? ShoppingCartItems { get; set; }
private ShoppingCart(FakePieShopDbContext fakePieShopDbContext)
{
_fakePieShopDbContext = fakePieShopDbContext;
}
public static ShoppingCart GetCart(IServiceProvider services)
{
ISession? session = services.GetRequiredService<IHttpContextAccessor>()?
.HttpContext?.Session;
FakePieShopDbContext context = services.GetService<FakePieShopDbContext>() ?? throw new Exception("Unable to initialize DB context");
// check for an existing cart; if there is no session, or a cart with this id does not exist,
// generate a new GUID
string cartId = session?.GetString("CartId") ?? Guid.NewGuid().ToString();
// update the session with the new cart id, if one was generated
session?.SetString("CartId", cartId);
return new ShoppingCart(context) { ShoppingCartId = cartId };
}
private ShoppingCartItem? FindItemInCart(Pie pie)
{
return _fakePieShopDbContext.ShoppingCartItems.SingleOrDefault(
s => s.Pie.PieId == pie.PieId && s.ShoppingCartId == ShoppingCartId);
}
public void AddToCart(Pie pie)
{
ShoppingCartItem? shoppingCartItem = FindItemInCart(pie);
if (shoppingCartItem == null)
{
ShoppingCartItem newItem = new()
{
ShoppingCartId = ShoppingCartId,
Pie = pie,
Amount = 1,
};
_fakePieShopDbContext.ShoppingCartItems.Add(newItem);
}
else
{
shoppingCartItem.Amount++;
}
_fakePieShopDbContext.SaveChanges();
}
public int RemoveFromCart(Pie pie)
{
ShoppingCartItem? shoppingCartItem = FindItemInCart(pie);
int itemCount = 0;
if (shoppingCartItem != null)
{
if (shoppingCartItem.Amount > 1)
{
shoppingCartItem.Amount--;
itemCount = shoppingCartItem.Amount;
}
else
{
_fakePieShopDbContext.ShoppingCartItems.Remove(shoppingCartItem);
}
}
_fakePieShopDbContext.SaveChanges();
return itemCount;
}
public List<ShoppingCartItem> GetShoppingCartItems()
{
// if we have shopping cart items defined already, use that.
// otherwise, we querty the database for these items, and then
// assign them to our instance state.
return ShoppingCartItems ??=
_fakePieShopDbContext.ShoppingCartItems.Where(cart =>
cart.ShoppingCartId == ShoppingCartId)
.Include(s => s.Pie)
.ToList();
}
public void ClearCart()
{
var items = _fakePieShopDbContext.ShoppingCartItems
.Where(cart => cart.ShoppingCartId == ShoppingCartId);
_fakePieShopDbContext.RemoveRange(items);
_fakePieShopDbContext.SaveChanges();
}
public decimal GetShoppingCartTotal()
{
return _fakePieShopDbContext.ShoppingCartItems.Where
// all the items associated with this cart
(item => item.ShoppingCartId == ShoppingCartId)
// the product of price * amount for each of these
.Select(item => item.Pie.Price * item.Amount)
// finally, the sum of these calculations.
.Sum();
}
}
}

View File

@@ -0,0 +1,10 @@
namespace FakePieShop.Models
{
public class ShoppingCartItem
{
public int ShoppingCartItemId { get; set; }
public Pie Pie { get; set; } = default!;
public int Amount { get; set; }
public string? ShoppingCartId { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
namespace FakePieShop.Models.ViewModels
{
public class ShoppingCartViewModel
{
public ShoppingCartViewModel(IShoppingCart shoppingCart, decimal shoppingCartTotal)
{
ShoppingCart = shoppingCart;
ShoppingCartTotal = shoppingCartTotal;
}
public IShoppingCart ShoppingCart { get; }
public decimal ShoppingCartTotal { get; }
}
}

View File

@@ -7,7 +7,13 @@ WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// add services to application // add services to application
builder.Services.AddScoped<IPieRepository, PieRepository>(); builder.Services.AddScoped<IPieRepository, PieRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>(); builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<IShoppingCart, ShoppingCart>(services => ShoppingCart.GetCart(services));
// include support for sessions and http context
builder.Services.AddSession();
builder.Services.AddHttpContextAccessor();
// configuration for MVC and entity framework core
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<FakePieShopDbContext>(options => builder.Services.AddDbContext<FakePieShopDbContext>(options =>
{ {
@@ -15,11 +21,11 @@ builder.Services.AddDbContext<FakePieShopDbContext>(options =>
builder.Configuration["ConnectionStrings:FakePieShopDbContextConnection"]); builder.Configuration["ConnectionStrings:FakePieShopDbContextConnection"]);
}); });
WebApplication app = builder.Build(); var app = builder.Build();
// middlewares // middlewares
app.UseStaticFiles(); app.UseStaticFiles();
//app.UseAuthentication(); app.UseSession();
// dx and debugging // dx and debugging
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())

View File

@@ -5,6 +5,11 @@
<img src="@Model.ImageThumbnailUrl" class="card-img-top" alt="@Model.Name"> <img src="@Model.ImageThumbnailUrl" class="card-img-top" alt="@Model.Name">
<div class="card-body pie-button"> <div class="card-body pie-button">
<h4 class="d-grid"> <h4 class="d-grid">
<a class="btn btn-secondary"
asp-controller="ShoppingCart"
asp-action="AddToShoppingCart"
asp-route-pieId="@Model.PieId"> + Add to cart
</a>
</h4> </h4>
<div class="d-flex justify-content-between mt-2"> <div class="d-flex justify-content-between mt-2">

View File

@@ -0,0 +1,18 @@
@model ShoppingCartItem
<div class="card shopping-cart-card mb-2">
<div class="row">
<div class="col-md-4">
<img src="@Model.Pie.ImageThumbnailUrl" class="img-fluid rounded-start p-2" alt="@Model.Pie.Name">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-text">@Model.Amount x @Model.Pie.Name</h5>
<div class="d-flex justify-content-between">
<h6>@Model.Pie.ShortDescription</h6>
<h2>@Model.Pie.Price.ToString("c")</h2>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@model ShoppingCartViewModel
<h3 class="my-5">
Shopping cart
</h3>
<div class="row gx-3">
<div class="col-8">
@foreach (var line in Model.ShoppingCart.ShoppingCartItems)
{
<partial name="_ShoppingCartItemCard" model="line" />
}
</div>
<div class="col-4">
<div class="card shopping-cart-card p-3">
<div class="row">
<h4 class="col">Total:</h4>
<h4 class="col text-end">@Model.ShoppingCartTotal.ToString("c")</h4>
</div>
<hr />
<div class="text-center d-grid">
</div>
</div>
</div>
</div>

View File

@@ -40,6 +40,14 @@ build_metadata.AdditionalFiles.CssScope =
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9QaWVDYXJkLmNzaHRtbA== build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9QaWVDYXJkLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_ShoppingCartItemCard.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9TaG9wcGluZ0NhcnRJdGVtQ2FyZC5jc2h0bWw=
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/ShoppingCart/Index.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hvcHBpbmdDYXJ0XEluZGV4LmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/_ViewImports.cshtml] [C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/_ViewImports.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcX1ZpZXdJbXBvcnRzLmNzaHRtbA== build_metadata.AdditionalFiles.TargetPath = Vmlld3NcX1ZpZXdJbXBvcnRzLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =

File diff suppressed because one or more lines are too long

View File

@@ -28,10 +28,26 @@ build_metadata.AdditionalFiles.CssScope =
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGllXExpc3QuY3NodG1s build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGllXExpc3QuY3NodG1s
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_Carousel.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9DYXJvdXNlbC5jc2h0bWw=
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_Layout.cshtml] [C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_Layout.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9MYXlvdXQuY3NodG1s build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9MYXlvdXQuY3NodG1s
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_PieCard.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9QaWVDYXJkLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/Shared/_ShoppingCartItemCard.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9TaG9wcGluZ0NhcnRJdGVtQ2FyZC5jc2h0bWw=
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/ShoppingCart/Index.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hvcHBpbmdDYXJ0XEluZGV4LmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope =
[C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/_ViewImports.cshtml] [C:/Users/mikay/source/repos/FakePieShop/FakePieShop/Views/_ViewImports.cshtml]
build_metadata.AdditionalFiles.TargetPath = Vmlld3NcX1ZpZXdJbXBvcnRzLmNzaHRtbA== build_metadata.AdditionalFiles.TargetPath = Vmlld3NcX1ZpZXdJbXBvcnRzLmNzaHRtbA==
build_metadata.AdditionalFiles.CssScope = build_metadata.AdditionalFiles.CssScope =