diff --git a/csharp-ms/.gitignore b/csharp-ms/.gitignore index a3325fb..c6c412f 100644 --- a/csharp-ms/.gitignore +++ b/csharp-ms/.gitignore @@ -3,3 +3,5 @@ bin obj packages *.sln.DotSettings.user +# Ignore Blazor wwwroot/lib folder (Bootstrap and other libraries) +**/wwwroot/lib/ diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/App.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/App.razor new file mode 100644 index 0000000..9a15697 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/App.razor @@ -0,0 +1,23 @@ +๏ปฟ + + + + + + + + + + + + + + + + + + + + + + diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..78624f3 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor @@ -0,0 +1,23 @@ +๏ปฟ@inherits LayoutComponentBase + +
+ + +
+
+ About +
+ +
+ @Body +
+
+
+ +
+ An unhandled error has occurred. + Reload + ๐Ÿ—™ +
diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor.css b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..38d1f25 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/MainLayout.razor.css @@ -0,0 +1,98 @@ +.page { + position: relative; + display: flex; + flex-direction: column; +} + +main { + flex: 1; +} + +.sidebar { + background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); +} + +.top-row { + background-color: #f7f7f7; + border-bottom: 1px solid #d6d5d5; + justify-content: flex-end; + height: 3.5rem; + display: flex; + align-items: center; +} + + .top-row ::deep a, .top-row ::deep .btn-link { + white-space: nowrap; + margin-left: 1.5rem; + text-decoration: none; + } + + .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { + text-decoration: underline; + } + + .top-row ::deep a:first-child { + overflow: hidden; + text-overflow: ellipsis; + } + +@media (max-width: 640.98px) { + .top-row { + justify-content: space-between; + } + + .top-row ::deep a, .top-row ::deep .btn-link { + margin-left: 0; + } +} + +@media (min-width: 641px) { + .page { + flex-direction: row; + } + + .sidebar { + width: 250px; + height: 100vh; + position: sticky; + top: 0; + } + + .top-row { + position: sticky; + top: 0; + z-index: 1; + } + + .top-row.auth ::deep a:first-child { + flex: 1; + text-align: right; + width: 0; + } + + .top-row, article { + padding-left: 2rem !important; + padding-right: 1.5rem !important; + } +} + +#blazor-error-ui { + color-scheme: light only; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..aa7bf6c --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor @@ -0,0 +1,18 @@ + + + + + + diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor.css b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..a2aeace --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + min-height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor new file mode 100644 index 0000000..49d916b --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor @@ -0,0 +1,31 @@ +๏ปฟ + + +
+ +

+ Rejoining the server... +

+

+ Rejoin failed... trying again in seconds. +

+

+ Failed to rejoin.
Please retry or reload the page. +

+ +

+ The session has been paused by the server. +

+ +

+ Failed to resume the session.
Please reload the page. +

+
+
diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.css b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.css new file mode 100644 index 0000000..3ad3773 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.css @@ -0,0 +1,157 @@ +.components-reconnect-first-attempt-visible, +.components-reconnect-repeated-attempt-visible, +.components-reconnect-failed-visible, +.components-pause-visible, +.components-resume-failed-visible, +.components-rejoining-animation { + display: none; +} + +#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible, +#components-reconnect-modal.components-reconnect-show .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-paused .components-pause-visible, +#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible, +#components-reconnect-modal.components-reconnect-retrying, +#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible, +#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation, +#components-reconnect-modal.components-reconnect-failed, +#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible { + display: block; +} + + +#components-reconnect-modal { + background-color: white; + width: 20rem; + margin: 20vh auto; + padding: 2rem; + border: 0; + border-radius: 0.5rem; + box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); + opacity: 0; + transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; + animation: components-reconnect-modal-fadeOutOpacity 0.5s both; + &[open] + +{ + animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s; + animation-fill-mode: both; +} + +} + +#components-reconnect-modal::backdrop { + background-color: rgba(0, 0, 0, 0.4); + animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; + opacity: 1; +} + +@keyframes components-reconnect-modal-slideUp { + 0% { + transform: translateY(30px) scale(0.95); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes components-reconnect-modal-fadeInOpacity { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes components-reconnect-modal-fadeOutOpacity { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.components-reconnect-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +#components-reconnect-modal p { + margin: 0; + text-align: center; +} + +#components-reconnect-modal button { + border: 0; + background-color: #6b9ed2; + color: white; + padding: 4px 24px; + border-radius: 4px; +} + + #components-reconnect-modal button:hover { + background-color: #3b6ea2; + } + + #components-reconnect-modal button:active { + background-color: #6b9ed2; + } + +.components-rejoining-animation { + position: relative; + width: 80px; + height: 80px; +} + + .components-rejoining-animation div { + position: absolute; + border: 3px solid #0087ff; + opacity: 1; + border-radius: 50%; + animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; + } + + .components-rejoining-animation div:nth-child(2) { + animation-delay: -0.5s; + } + +@keyframes components-rejoining-animation { + 0% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 4.9% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 0; + } + + 5% { + top: 40px; + left: 40px; + width: 0; + height: 0; + opacity: 1; + } + + 100% { + top: 0px; + left: 0px; + width: 80px; + height: 80px; + opacity: 0; + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.js b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.js new file mode 100644 index 0000000..e52a190 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Layout/ReconnectModal.razor.js @@ -0,0 +1,63 @@ +// Set up event handlers +const reconnectModal = document.getElementById("components-reconnect-modal"); +reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged); + +const retryButton = document.getElementById("components-reconnect-button"); +retryButton.addEventListener("click", retry); + +const resumeButton = document.getElementById("components-resume-button"); +resumeButton.addEventListener("click", resume); + +function handleReconnectStateChanged(event) { + if (event.detail.state === "show") { + reconnectModal.showModal(); + } else if (event.detail.state === "hide") { + reconnectModal.close(); + } else if (event.detail.state === "failed") { + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } else if (event.detail.state === "rejected") { + location.reload(); + } +} + +async function retry() { + document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + + try { + // Reconnect will asynchronously return: + // - true to mean success + // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID) + // - exception to mean we didn't reach the server (this can be sync or async) + const successful = await Blazor.reconnect(); + if (!successful) { + // We have been able to reach the server, but the circuit is no longer available. + // We'll reload the page so the user can continue using the app as quickly as possible. + const resumeSuccessful = await Blazor.resumeCircuit(); + if (!resumeSuccessful) { + location.reload(); + } else { + reconnectModal.close(); + } + } + } catch (err) { + // We got an exception, server is currently unavailable + document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible); + } +} + +async function resume() { + try { + const successful = await Blazor.resumeCircuit(); + if (!successful) { + location.reload(); + } + } catch { + location.reload(); + } +} + +async function retryWhenDocumentBecomesVisible() { + if (document.visibilityState === "visible") { + await retry(); + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Counter.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Counter.razor new file mode 100644 index 0000000..1a4f8e7 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Counter.razor @@ -0,0 +1,19 @@ +๏ปฟ@page "/counter" +@rendermode InteractiveServer + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Error.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +๏ปฟ@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

+ Swapping to Development environment will display more detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

+ +@code{ + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Home.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Home.razor new file mode 100644 index 0000000..83b675f --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Home.razor @@ -0,0 +1,246 @@ +@page "/" +@rendermode InteractiveServer +@inject SupermarketService SupermarketService + +Parrot Supermarket ๐Ÿฆœ + +
+
+

๐Ÿฆœ Parrot Supermarket ๐Ÿฆœ

+

Welcome to the Parrot-themed Supermarket Receipt App!

+
+ +
+ +
+
+
+

๐Ÿฆœ Products

+
+
+ @foreach (var product in _products) + { +
+
+
+
@product.Name
+

+ Price: ยฃ@SupermarketService.GetProductPrice(product).ToString("F2") + @if (product.Unit == ProductUnit.Kilo) + { + per kg + } + else + { + each + } +

+
+
+ + +
+
+
+ } +
+
+ + +
+
+

๐Ÿฆœ Special Offers

+
+
+ @foreach (var product in _products) + { +
+ +
+ + @if (_selectedOfferTypes[product.Name] == "TwoForAmount" || + _selectedOfferTypes[product.Name] == "FiveForAmount") + { + + } + else if (_selectedOfferTypes[product.Name] == "TenPercentDiscount") + { + + } + +
+
+ } +
+
+
+ + +
+
+
+

๐Ÿฆœ Shopping Cart

+
+
+ @if (_cartItems.Any()) + { +
    + @foreach (var item in _cartItems) + { + var price = SupermarketService.GetProductPrice(item.Product) * item.Quantity; +
  • + @item.Product.Name (x@(item.Quantity)) + ยฃ@(price.ToString("F2")) +
  • + } +
+
+ + +
+ } + else + { +

Your cart is empty. Add some items! ๐Ÿฆœ

+ } +
+
+ + + @if (_receipt != null) + { +
+
+

๐Ÿฆœ Receipt

+
+
+
+
@_receiptText
+
+
+

Total: ยฃ@_receipt.GetTotalPrice().ToString("F2")

+
+ +
+
+ } +
+
+
+ +@code { + private List _products = new(); + private Dictionary _quantities = new(); + private Dictionary _selectedOfferTypes = new(); + private Dictionary _offerArguments = new(); + private List _cartItems = new(); + private Receipt? _receipt; + private string _receiptText = ""; + + protected override void OnInitialized() + { + _products = SupermarketService.GetAllProducts(); + + // Initialize dictionaries + foreach (var product in _products) + { + _quantities[product.Name] = 1.0; + _selectedOfferTypes[product.Name] = ""; + _offerArguments[product.Name] = 0.0; + } + + UpdateCartItems(); + } + + private void AddToCart(Product product) + { + var quantity = _quantities[product.Name]; + if (quantity > 0) + { + SupermarketService.AddToCart(product, quantity); + UpdateCartItems(); + } + } + + private void ApplyOffer(Product product) + { + var offerTypeStr = _selectedOfferTypes[product.Name]; + if (!string.IsNullOrEmpty(offerTypeStr)) + { + var offerType = Enum.Parse(offerTypeStr); + var argument = _offerArguments[product.Name]; + + if (offerType == SpecialOfferType.TenPercentDiscount) + { + argument = argument > 0 ? argument : 10.0; + } + else if (offerType == SpecialOfferType.TwoForAmount || offerType == SpecialOfferType.FiveForAmount) + { + if (argument <= 0) + { + return; // Need a valid amount + } + } + + SupermarketService.AddSpecialOffer(offerType, product, argument); + } + } + + private void UpdateCartItems() + { + _cartItems = SupermarketService.GetCart().GetItems(); + } + + private void Checkout() + { + _receipt = SupermarketService.CheckOut(); + var printer = new ReceiptPrinter(); + _receiptText = printer.PrintReceipt(_receipt); + } + + private void ClearCart() + { + SupermarketService.ResetCart(); + UpdateCartItems(); + } + + private void ClearReceipt() + { + _receipt = null; + _receiptText = ""; + SupermarketService.ResetCart(); + UpdateCartItems(); + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/NotFound.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/NotFound.razor new file mode 100644 index 0000000..917ada1 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/NotFound.razor @@ -0,0 +1,5 @@ +๏ปฟ@page "/not-found" +@layout MainLayout + +

Not Found

+

Sorry, the content you are looking for does not exist.

\ No newline at end of file diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Weather.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Weather.razor new file mode 100644 index 0000000..f437e5e --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Pages/Weather.razor @@ -0,0 +1,64 @@ +๏ปฟ@page "/weather" +@attribute [StreamRendering] + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate streaming rendering + await Task.Delay(500); + + var startDate = DateOnly.FromDateTime(DateTime.Now); + var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; + forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = startDate.AddDays(index), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = summaries[Random.Shared.Next(summaries.Length)] + }).ToArray(); + } + + private class WeatherForecast + { + public DateOnly Date { get; set; } + public int TemperatureC { get; set; } + public string? Summary { get; set; } + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/Routes.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Routes.razor new file mode 100644 index 0000000..105855d --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/Routes.razor @@ -0,0 +1,6 @@ +๏ปฟ + + + + + diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Components/_Imports.razor b/csharp-ms/SupermarketReceipt.BlazorApp/Components/_Imports.razor new file mode 100644 index 0000000..619f62d --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using SupermarketReceipt.BlazorApp +@using SupermarketReceipt.BlazorApp.Components +@using SupermarketReceipt.BlazorApp.Components.Layout +@using SupermarketReceipt +@using SupermarketReceipt.BlazorApp.Services diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Program.cs b/csharp-ms/SupermarketReceipt.BlazorApp/Program.cs new file mode 100644 index 0000000..8587667 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Program.cs @@ -0,0 +1,31 @@ +using SupermarketReceipt.BlazorApp.Components; +using SupermarketReceipt.BlazorApp.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + +// Add SupermarketService as a singleton +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} +app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); +app.UseHttpsRedirection(); + +app.UseAntiforgery(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Properties/launchSettings.json b/csharp-ms/SupermarketReceipt.BlazorApp/Properties/launchSettings.json new file mode 100644 index 0000000..77879c9 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7156;http://localhost:5023", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/README.md b/csharp-ms/SupermarketReceipt.BlazorApp/README.md new file mode 100644 index 0000000..d0e73b9 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/README.md @@ -0,0 +1,93 @@ +# ๐Ÿฆœ Parrot Supermarket - Blazor Frontend + +A fun, parrot-themed Blazor web application for the Supermarket Receipt kata. + +## Features + +- ๐Ÿฆœ **Parrot-themed UI** - All products and UI elements feature extra parrots as requested! +- ๐Ÿ›’ **Product Catalog** - Browse and add products to your shopping cart +- ๐ŸŽฏ **Special Offers** - Configure and apply various types of special offers: + - 3 for 2 deals + - 10% discount + - 2 for amount deals + - 5 for amount deals +- ๐Ÿงพ **Receipt Generation** - Automatically generates receipts with applied discounts +- ๐Ÿ’ฐ **Real-time Pricing** - See totals update as you add items and apply offers + +## Running the Application + +### Prerequisites +- .NET 10 SDK or later + +### Steps + +1. Navigate to the Blazor app directory: + ```bash + cd csharp-ms/SupermarketReceipt.BlazorApp + ``` + +2. Run the application: + ```bash + dotnet run + ``` + +3. Open your browser and navigate to the URL shown in the console (typically `http://localhost:5023` or `https://localhost:7023`) + +### Alternative: Run from Solution + +You can also run the entire solution from Visual Studio or using: +```bash +cd csharp-ms +dotnet run --project SupermarketReceipt.BlazorApp +``` + +## How to Use + +1. **Add Products to Cart**: + - Adjust the quantity for any product + - Click "Add to Cart" to add items to your shopping cart + +2. **Apply Special Offers**: + - Select an offer type from the dropdown for any product + - For "2 for Amount" or "5 for Amount", enter the special price + - For "10% Discount", enter the percentage (default 10%) + - Click "Apply" to activate the offer + +3. **Checkout**: + - Review your cart items + - Click the "๐Ÿฆœ Checkout" button to generate your receipt + - The receipt will show all items, applied discounts, and the total + +4. **New Transaction**: + - Click "New Transaction" to clear the receipt and cart + - Or use "Clear" to empty just the cart + +## Architecture + +The application is built using: +- **Blazor Server** - For interactive server-side rendering +- **Bootstrap 5** - For responsive styling +- **SupermarketReceipt Library** - Core business logic for pricing and discounts + +### Project Structure + +``` +SupermarketReceipt.BlazorApp/ +โ”œโ”€โ”€ Components/ +โ”‚ โ”œโ”€โ”€ Layout/ # Navigation and layout components +โ”‚ โ””โ”€โ”€ Pages/ # Page components (Home.razor) +โ”œโ”€โ”€ Services/ # SupermarketService for state management +โ”œโ”€โ”€ wwwroot/ # Static assets and CSS +โ””โ”€โ”€ Program.cs # Application startup +``` + +## Notes + +- The application uses a singleton service to maintain catalog and cart state +- All products are pre-loaded with sample data featuring parrot emojis ๐Ÿฆœ +- The receipt printer reuses the existing `ReceiptPrinter` class from the core library +- Special offers are applied at checkout time + +## Extra Parrots! ๐Ÿฆœ๐Ÿฆœ๐Ÿฆœ + +As requested, this application features extra parrots throughout the UI for a fun, tropical shopping experience! diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/Services/SupermarketService.cs b/csharp-ms/SupermarketReceipt.BlazorApp/Services/SupermarketService.cs new file mode 100644 index 0000000..558b482 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/Services/SupermarketService.cs @@ -0,0 +1,76 @@ +using SupermarketReceipt; + +namespace SupermarketReceipt.BlazorApp.Services +{ + public class SupermarketService + { + private readonly FakeCatalog _catalog; + private readonly Teller _teller; + private ShoppingCart _cart; + private readonly List<(Product Product, double Price)> _availableProducts; + + public SupermarketService() + { + _catalog = new FakeCatalog(); + _teller = new Teller(_catalog); + _cart = new ShoppingCart(); + + // Define available products ๐Ÿฆœ + _availableProducts = new List<(Product, double)> + { + (new Product("Parrot Toothbrush ๐Ÿฆœ", ProductUnit.Each), 0.99), + (new Product("Tropical Apples ๐Ÿฆœ", ProductUnit.Kilo), 1.99), + (new Product("Parrot Rice ๐Ÿฆœ", ProductUnit.Each), 2.49), + (new Product("Parrot Toothpaste ๐Ÿฆœ", ProductUnit.Each), 1.79), + (new Product("Cherry Tomatoes ๐Ÿฆœ", ProductUnit.Each), 0.69), + (new Product("Parrot Milk ๐Ÿฆœ", ProductUnit.Each), 1.29) + }; + + // Initialize catalog with products + InitializeCatalog(); + } + + private void InitializeCatalog() + { + foreach (var (product, price) in _availableProducts) + { + _catalog.AddProduct(product, price); + } + } + + public SupermarketCatalog GetCatalog() => _catalog; + + public ShoppingCart GetCart() => _cart; + + public void ResetCart() + { + _cart = new ShoppingCart(); + } + + public void AddToCart(Product product, double quantity) + { + _cart.AddItemQuantity(product, quantity); + } + + public void AddSpecialOffer(SpecialOfferType offerType, Product product, double argument) + { + _teller.AddSpecialOffer(offerType, product, argument); + } + + public Receipt CheckOut() + { + var receipt = _teller.ChecksOutArticlesFrom(_cart); + return receipt; + } + + public List GetAllProducts() + { + return _availableProducts.Select(p => p.Product).ToList(); + } + + public double GetProductPrice(Product product) + { + return _catalog.GetUnitPrice(product); + } + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/SupermarketReceipt.BlazorApp.csproj b/csharp-ms/SupermarketReceipt.BlazorApp/SupermarketReceipt.BlazorApp.csproj new file mode 100644 index 0000000..db7f6e1 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/SupermarketReceipt.BlazorApp.csproj @@ -0,0 +1,14 @@ + + + + + + + + net10.0 + enable + enable + true + + + diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.Development.json b/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.json b/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/app.css b/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/app.css new file mode 100644 index 0000000..0bb93c2 --- /dev/null +++ b/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/app.css @@ -0,0 +1,123 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +a, .btn-link { + color: #006bb7; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { + box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; +} + +.content { + padding-top: 1.1rem; +} + +h1:focus { + outline: none; +} + +.valid.modified:not([type=checkbox]) { + outline: 1px solid #26b050; +} + +.invalid { + outline: 1px solid #e50000; +} + +.validation-message { + color: #e50000; +} + +.blazor-error-boundary { + background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; + padding: 1rem 1rem 1rem 3.7rem; + color: white; +} + + .blazor-error-boundary::after { + content: "An error has occurred." + } + +.darker-border-checkbox.form-check-input { + border-color: #929292; +} + +.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { + color: var(--bs-secondary-color); + text-align: end; +} + +.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { + text-align: start; +} + +/* Parrot Supermarket Custom Styles ๐Ÿฆœ */ +.product-item { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + transition: all 0.3s ease; +} + +.product-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%); +} + +.card { + transition: all 0.3s ease; +} + +.card:hover { + box-shadow: 0 8px 16px rgba(0,0,0,0.15) !important; +} + +.receipt-content { + background-image: + repeating-linear-gradient( + 45deg, + transparent, + transparent 10px, + rgba(255,255,255,.05) 10px, + rgba(255,255,255,.05) 20px + ); +} + +.btn-success { + background-color: #28a745; + border-color: #28a745; +} + +.btn-success:hover { + background-color: #218838; + border-color: #1e7e34; +} + +.card-header { + font-weight: bold; + font-size: 1.1rem; +} + +/* Parrot theme colors */ +.bg-success { + background-color: #20c997 !important; +} + +.bg-warning { + background-color: #ffc107 !important; +} + +.bg-primary { + background-color: #007bff !important; +} + +.bg-info { + background-color: #17a2b8 !important; +} \ No newline at end of file diff --git a/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/favicon.png b/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/csharp-ms/SupermarketReceipt.BlazorApp/wwwroot/favicon.png differ diff --git a/csharp-ms/SupermarketReceipt.sln b/csharp-ms/SupermarketReceipt.sln index 59db09a..9fccd34 100644 --- a/csharp-ms/SupermarketReceipt.sln +++ b/csharp-ms/SupermarketReceipt.sln @@ -7,20 +7,54 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupermarketReceipt", "Super EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupermarketReceipt.Test", "SupermarketReceipt.Test\SupermarketReceipt.Test.csproj", "{B8789108-5351-4785-836A-6E00C6AF3596}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SupermarketReceipt.BlazorApp", "SupermarketReceipt.BlazorApp\SupermarketReceipt.BlazorApp.csproj", "{E857982C-2949-4F66-801E-748111A07517}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|x64.Build.0 = Debug|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Debug|x86.Build.0 = Debug|Any CPU {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|Any CPU.Build.0 = Release|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|x64.ActiveCfg = Release|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|x64.Build.0 = Release|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|x86.ActiveCfg = Release|Any CPU + {B384010A-E2A8-4720-A622-DC49A1CC76D4}.Release|x86.Build.0 = Release|Any CPU {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|x64.Build.0 = Debug|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Debug|x86.Build.0 = Debug|Any CPU {B8789108-5351-4785-836A-6E00C6AF3596}.Release|Any CPU.ActiveCfg = Release|Any CPU {B8789108-5351-4785-836A-6E00C6AF3596}.Release|Any CPU.Build.0 = Release|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Release|x64.ActiveCfg = Release|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Release|x64.Build.0 = Release|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Release|x86.ActiveCfg = Release|Any CPU + {B8789108-5351-4785-836A-6E00C6AF3596}.Release|x86.Build.0 = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|x64.ActiveCfg = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|x64.Build.0 = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|x86.ActiveCfg = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Debug|x86.Build.0 = Debug|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|Any CPU.Build.0 = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|x64.ActiveCfg = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|x64.Build.0 = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|x86.ActiveCfg = Release|Any CPU + {E857982C-2949-4F66-801E-748111A07517}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE