This commit is contained in:
Eugenio Chiodo 2021-04-26 15:35:44 +02:00
parent 9182a1051c
commit f537d097ec
84 changed files with 43538 additions and 0 deletions

49
drinkMe.sln Normal file
View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31129.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "drinkMe.Server", "drinkMe\Server\drinkMe.Server.csproj", "{4347ED6F-C6EC-44D2-A718-68490CBFFDF0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "drinkMe.Client", "drinkMe\Client\drinkMe.Client.csproj", "{33E45585-7F5D-428D-8A27-FAB29A1A2E61}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "drinkMe.Shared", "drinkMe\Shared\drinkMe.Shared.csproj", "{85CCE095-22AC-434E-B77C-75998FF8DAD3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "drinkMe.Client.Models", "drinkMe\drinkMe.Client.Models\drinkMe.Client.Models.csproj", "{7C00959D-3C7F-4617-B1DA-7909E5879C08}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "drinkMe.Server.Models", "drinkMe\drinkMe.Server.Models\drinkMe.Server.Models.csproj", "{EC73DC9D-FD40-4505-989D-83B49018401D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4347ED6F-C6EC-44D2-A718-68490CBFFDF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4347ED6F-C6EC-44D2-A718-68490CBFFDF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4347ED6F-C6EC-44D2-A718-68490CBFFDF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4347ED6F-C6EC-44D2-A718-68490CBFFDF0}.Release|Any CPU.Build.0 = Release|Any CPU
{33E45585-7F5D-428D-8A27-FAB29A1A2E61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33E45585-7F5D-428D-8A27-FAB29A1A2E61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33E45585-7F5D-428D-8A27-FAB29A1A2E61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33E45585-7F5D-428D-8A27-FAB29A1A2E61}.Release|Any CPU.Build.0 = Release|Any CPU
{85CCE095-22AC-434E-B77C-75998FF8DAD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85CCE095-22AC-434E-B77C-75998FF8DAD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85CCE095-22AC-434E-B77C-75998FF8DAD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85CCE095-22AC-434E-B77C-75998FF8DAD3}.Release|Any CPU.Build.0 = Release|Any CPU
{7C00959D-3C7F-4617-B1DA-7909E5879C08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C00959D-3C7F-4617-B1DA-7909E5879C08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C00959D-3C7F-4617-B1DA-7909E5879C08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C00959D-3C7F-4617-B1DA-7909E5879C08}.Release|Any CPU.Build.0 = Release|Any CPU
{EC73DC9D-FD40-4505-989D-83B49018401D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC73DC9D-FD40-4505-989D-83B49018401D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC73DC9D-FD40-4505-989D-83B49018401D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC73DC9D-FD40-4505-989D-83B49018401D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AB58BAA9-F7FC-4BF0-BC33-174BAB7F8320}
EndGlobalSection
EndGlobal

10
drinkMe/Client/App.razor Normal file
View File

@ -0,0 +1,10 @@
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -0,0 +1,35 @@
<section class="section has-content-centered">
<ul class="steps is-centered has-content-centered is-medium">
<li class="steps-segment @VUtilities.IfTrueThen(PurchaseStep == PurchaseStep.ChoosingProducts, ActiveClass, InactiveClass)">
<span class="steps-marker is-primary">
<i class="fad fa-cart-plus"></i>
</span>
<div class="steps-content">
<p class="is-size-4">Choose drinks</p>
</div>
</li>
<li class="steps-segment @VUtilities.IfTrueThen(PurchaseStep == PurchaseStep.PaymentSetup, ActiveClass, InactiveClass)">
<span class="steps-marker @VUtilities.IfTrueThen(PurchaseStep is PurchaseStep.PaymentSetup or PurchaseStep.SuccessfulPurchase, PrimaryClass)">
<i class="fad fa-money-bill-wave"></i>
</span>
<div class="steps-content">
<p class="is-size-4">Payment</p>
</div>
</li>
<li class="steps-segment @VUtilities.IfTrueThen(PurchaseStep == PurchaseStep.SuccessfulPurchase, ActiveClass, InactiveClass)">
<span class="steps-marker @VUtilities.IfTrueThen(PurchaseStep == PurchaseStep.SuccessfulPurchase, PrimaryClass)">
<i class="fad fa-check-double"></i>
</span>
<div class="steps-content">
<p class="is-size-4">Done!</p>
</div>
</li>
</ul>
</section>
@code {
[Parameter] public PurchaseStep PurchaseStep { get; set; }
const string ActiveClass = "is-active has-gaps";
const string InactiveClass = "is-primary has-gaps";
const string PrimaryClass = "is-primary";
}

View File

@ -0,0 +1,13 @@
namespace drinkMe.Client.Helpers
{
public static class VUtilities
{
public static T IfTrueThen<T>(bool conditionIsTrue, T ifTrue, T ifFalse = default)
{
if (conditionIsTrue)
return ifTrue;
else
return ifFalse;
}
}
}

View File

@ -0,0 +1,23 @@
@page "/done"
<PurchaseSteps PurchaseStep="PurchaseStep.SuccessfulPurchase" />
<section class="section">
<div class="container">
<div class="columns">
<div class="column">
<div class="box">
<h1 class="title has-text-centered">
<span>🎉</span>
<span>Purchase completed with success!</span>
<span>🎉</span>
</h1>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,163 @@
@page "/"
<PurchaseSteps PurchaseStep="PurchaseStep.ChoosingProducts" />
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-half">
<div class="columns is-multiline">
@foreach (var drink in AvailableDrinks)
{
<div @key="drink.Id" class="column is-full">
<div class="box">
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img src="images/drinks/@drink.PictureName" alt="drink image">
</figure>
</div>
<div class="media-content">
<div class="content">
<p>
<strong>@drink.Name</strong>
<br>
<span>@drink.Description</span>
</p>
</div>
<div class="level">
<div class="level-left">
<div class="level-item">
<strong>@drink.Price €</strong>
</div>
</div>
<div class="level-right mt-2-mobile">
<div class="level-item has-text-centered">
<button class="button is-small is-primary has-icons-left"
disabled="@drink.IsInTheCart" @onclick="async () => await AddToCart(drink)">
<span class="icon"><i class="fad fa-cart-arrow-down"></i></span>
<span>Add to cart</span>
</button>
</div>
</div>
</div>
</div>
</article>
</div>
</div>
}
</div>
</div>
<div class="column is-half">
<div class="box">
@if (CartDrinks.Count == 0)
{
<h1 class="has-text-centered">
<span class="icon-text">
<span class="icon">
<i class="fad fa-shopping-cart"></i>
</span>
<span>Empty cart</span>
</span>
</h1>
}
else
{
<div class="drinkCartList">
@foreach (var cartDrink in CartDrinks)
{
<div class="drinkCartItem">
<div class="drinkCartName"><strong>@cartDrink.Name</strong></div>
<div class="drinkCartQuantity">
<div class="field has-addons">
<p class="control">
<button class="button is-small is-danger" @onclick="async () => await SubtractQuantity(cartDrink)">
<span class="icon"><i class="fad fa-minus"></i></span>
</button>
</p>
<p class="control">
<input class="input is-small has-text-centered" type="text" readonly @bind-value="cartDrink.Quantity" />
</p>
<p class="control">
<button class="button is-small is-success" @onclick="async () => await AddQuantity(cartDrink)">
<span class="icon"><i class="fad fa-plus"></i></span>
</button>
</p>
</div>
</div>
<div class="drinkCartCross has-text-centered">
<span class="icon">
<i class="fad fa-times"></i>
</span>
</div>
<div class="drinkCartPrice has-text-centered">@cartDrink.Price €</div>
</div>
}
</div>
<hr />
<EditForm Model="DiscountForm" OnValidSubmit="TryAddDiscount">
<DataAnnotationsValidator />
<label class="label" for="discount">
<span>Any discount?</span>
</label>
<div class="field has-addons">
<p class="control is-expanded has-icons-left">
<span class="icon is-left"><i class="fad fa-tags"></i></span>
<input class="input" type="text" id="discount" placeholder="discount code" @onchange="OnChangeDiscountCode">
</p>
<p class="control">
<button type="submit" class="button is-primary has-icons-left">
<span class="icon is-left">
<i class="fad fa-badge-dollar"></i>
</span>
<span>Add discount</span>
</button>
</p>
</div>
<p class="help is-danger">
<ValidationMessage For="() => DiscountForm.DiscountCode" />
@InvalidDiscountCodeError
</p>
<ul>
@foreach (var discountCode in DiscountCodes)
{
<li class="has-text-right">
<span class="tag is-dark">@discountCode.Code</span>
<span class="tag is-success">-@discountCode.DiscountPercentage %</span>
</li>
}
</ul>
</EditForm>
<hr />
<div class="level">
<div class="level-left">
<h2><strong>Totale price</strong></h2>
</div>
<div class="level-right">
<h2>
<strong>@(Math.Round(TotalPrice, 2)) €</strong>
</h2>
</div>
</div>
<hr />
<div class="buttons is-right">
<NavLink href="payment" class="button is-primary has-icons-left">
<span class="icon"><i class="fad fa-money-bill-wave"></i></span>
<span>Go to payment</span>
</NavLink>
</div>
}
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,106 @@
using Blazored.LocalStorage;
using drinkMe.Client.Models;
using drinkMe.Client.Services.Interfaces;
using drinkMe.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Client.Pages
{
public partial class Index : ComponentBase
{
[Inject] ILocalStorageService LocalStorage { get; set; }
[Inject] IPriceService PriceService { get; set; }
[Inject] IDataService DataService { get; set; }
protected List<DrinkViewModel> AvailableDrinks { get; set; } = new List<DrinkViewModel>();
protected List<DiscountCodeViewModel> DiscountCodes { get; set; } = new List<DiscountCodeViewModel>();
protected List<DrinkViewModel> CartDrinks { get; set; } = new List<DrinkViewModel>();
protected string GetDrinksError { get; set; }
protected string InvalidDiscountCodeError { get; set; }
protected DiscountForm DiscountForm { get; set; } = new DiscountForm();
protected float TotalPrice { get; set; }
protected override async Task OnInitializedAsync()
{
var getDrinksResult = await DataService.GetDrinks();
if (!getDrinksResult.IsValid)
{
GetDrinksError = getDrinksResult.ErrorMessage;
return;
}
AvailableDrinks = (List<DrinkViewModel>)getDrinksResult.Data;
DiscountCodes = await LocalStorage.GetItemAsync<List<DiscountCodeViewModel>>(nameof(DiscountCodes)) ?? new List<DiscountCodeViewModel>();
CartDrinks = await LocalStorage.GetItemAsync<List<DrinkViewModel>>(nameof(CartDrinks)) ?? new List<DrinkViewModel>();
if (CartDrinks.Count > 0)
{
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
AvailableDrinks.ForEach(ad => ad.IsInTheCart = CartDrinks.Any(cd => cd.Id == ad.Id));
}
}
protected async Task AddQuantity(DrinkViewModel drink)
{
drink.Quantity = drink.Quantity is > 0 and < 20 ? ++drink.Quantity : drink.Quantity;
await LocalStorage.SetItemAsync(nameof(CartDrinks), CartDrinks);
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
}
protected async Task SubtractQuantity(DrinkViewModel drink)
{
if (drink.Quantity - 1 == 0)
{
var availableDrink = AvailableDrinks.First(ad => ad.Id == drink.Id);
availableDrink.IsInTheCart = false;
CartDrinks.Remove(drink);
await LocalStorage.SetItemAsync(nameof(CartDrinks), CartDrinks);
return;
}
drink.Quantity = drink.Quantity is > 1 and < 21 ? --drink.Quantity : drink.Quantity;
await LocalStorage.SetItemAsync(nameof(CartDrinks), CartDrinks);
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
}
protected async Task AddToCart(DrinkViewModel drink)
{
drink.IsInTheCart = true;
CartDrinks.Add(drink);
await LocalStorage.SetItemAsync(nameof(CartDrinks), CartDrinks);
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
}
protected async Task TryAddDiscount()
{
InvalidDiscountCodeError = default;
if (DiscountCodes.Any(dc => dc.Code == DiscountForm.DiscountCode))
{
InvalidDiscountCodeError = "Discount code already added.";
return;
}
var validationResult = await DataService.IsValidDiscountCode(DiscountForm.DiscountCode);
if (!validationResult.IsValid)
{
InvalidDiscountCodeError = validationResult.ErrorMessage;
return;
}
var discount = validationResult.Data as DiscountCodeViewModel;
DiscountCodes.Add(discount);
await LocalStorage.SetItemAsync(nameof(DiscountCodes), DiscountCodes);
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
}
protected void OnChangeDiscountCode(ChangeEventArgs changeEvent)
{
InvalidDiscountCodeError = default;
DiscountForm.DiscountCode = (string)changeEvent.Value;
}
}
}

View File

@ -0,0 +1,172 @@
@page "/payment"
<PurchaseSteps PurchaseStep="PurchaseStep.PaymentSetup" />
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-half">
<div class="box">
@if (CartDrinks.Count == 0)
{
<h1 class="has-text-centered">
<span class="icon-text">
<span class="icon">
<i class="fad fa-shopping-cart"></i>
</span>
<span>Empty cart</span>
</span>
</h1>
}
else
{
<div class="drinkCartList">
@foreach (var cartDrink in CartDrinks)
{
<div class="drinkCartPItem">
<div class="drinkCartName"><strong>@cartDrink.Name</strong></div>
<div class="drinkCartQuantity has-text-centered">
<div class="field">
<p class="control">
@cartDrink.Quantity
</p>
</div>
</div>
<div class="drinkCartCross has-text-centered">
<span class="icon">
<i class="fad fa-times"></i>
</span>
</div>
<div class="drinkCartPrice has-text-centered">@cartDrink.Price €</div>
</div>
}
</div>
if (DiscountCodes.Count > 0)
{
<hr />
<ul>
@foreach (var discountCode in DiscountCodes)
{
<li class="has-text-right">
<span class="tag is-dark">@discountCode.Code</span>
<span class="tag is-success">-@discountCode.DiscountPercentage %</span>
</li>
}
</ul>
}
<hr />
<div class="level">
<div class="level-left">
<h2><strong>Totale price</strong></h2>
</div>
<div class="level-right">
<h2>
<strong>@(Math.Round(TotalPrice, 2)) €</strong>
</h2>
</div>
</div>
}
</div>
</div>
<div class="column is-half">
<div class="box">
<EditForm Model="CreditCardForm" OnValidSubmit="Pay">
<DataAnnotationsValidator />
<InputRadioGroup Name="PaymentMethod" @bind-Value="PaymentMethod">
<div class="field">
<div class="control">
<label class="radio">
<InputRadio Name="PaymentMethod" Value="PaymentMethod.Cash" disabled="@(TotalPrice > 10f)" />
<span class="icon-text"><span class="icon"><i class="fad fa-money-bill-wave"></i></span> <span>I'm going with cash</span></span>
</label>
</div>
</div>
<div class="is-divider" data-content="OR"></div>
<div class="field">
<div class="control">
<label class="radio">
<InputRadio Name="PaymentMethod" Value="PaymentMethod.CreditCard" />
<span class="icon-text"><span class="icon"><i class="fad fa-credit-card-front"></i></span> <span>Here goes the card</span></span>
</label>
</div>
</div>
</InputRadioGroup>
<fieldset class="@VUtilities.IfTrueThen(PaymentMethod == PaymentMethod.Cash, "is-hidden")" disabled="@(PaymentMethod != PaymentMethod.CreditCard)">
<div class="field">
<label class="label">Credit card number</label>
<div class="control has-icons-left">
<span class="icon is-left"><i class="fad fa-credit-card-front"></i></span>
<input @bind-value="CreditCardForm.Number" class="input" placeholder="">
</div>
<p class="help is-danger">
<ValidationMessage For="() => CreditCardForm.Number" />
</p>
</div>
<div class="columns">
<div class="column is-half">
<label class="label">Expiration month / year</label>
<div class="field has-addons has-addons-right">
<div class="control">
<input @bind-value="CreditCardForm.ExpirationMonth" class="input has-text-centered" placeholder="01">
</div>
<div class="control">
<input class="input has-text-centered" value="/" readonly disabled>
</div>
<div class="control">
<input @bind-value="CreditCardForm.ExpirationYear" class="input has-text-centered" placeholder="@((DateTime.Now.Year + 1).ToString().Substring(2,2))">
</div>
</div>
<p class="help is-danger">
<ValidationMessage For="() => CreditCardForm.ExpirationMonth" />
<ValidationMessage For="() => CreditCardForm.ExpirationYear" />
</p>
</div>
<div class="column is-half">
<div class="field">
<label class="label">CVV code</label>
<div class="control has-icons-left">
<span class="icon is-left"><i class="fad fa-key"></i></span>
<input type="text" @bind-value="CreditCardForm.CVVCode" class="input has-text-centered" placeholder="***" />
</div>
<p class="help is-danger">
<ValidationMessage For="() => CreditCardForm.CVVCode" />
</p>
</div>
</div>
</div>
@if (PaymentMethod == PaymentMethod.CreditCard)
{
<div class="field has-text-right">
<button type="submit" class="button is-primary has-icons-left">
<span class="icon"><i class="fad fa-credit-card-blank"></i></span>
<span>Pay</span>
</button>
</div>
}
</fieldset>
</EditForm>
@if (PaymentMethod == PaymentMethod.Cash)
{
<div class="field mt-5 has-text-right">
<button class="button is-primary has-icons-left" @onclick="Pay">
<span class="icon"><i class="fad fa-money-bill-wave"></i></span>
<span>Pay at checkout</span>
</button>
</div>
}
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,69 @@
using Blazored.LocalStorage;
using drinkMe.Client.Models;
using drinkMe.Client.Services.Interfaces;
using drinkMe.Shared;
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Client.Pages
{
public partial class Payment : ComponentBase
{
[Inject] ILocalStorageService LocalStorage { get; set; }
[Inject] IPriceService PriceService { get; set; }
[Inject] IPaymentService PaymentService { get; set; }
[Inject] NavigationManager Navigation { get; set; }
protected List<DrinkViewModel> CartDrinks { get; set; } = new List<DrinkViewModel>();
protected List<DiscountCodeViewModel> DiscountCodes { get; set; } = new List<DiscountCodeViewModel>();
protected CreditCardForm CreditCardForm { get; set; } = new CreditCardForm();
protected PaymentMethod PaymentMethod { get; set; } = PaymentMethod.Undefined;
protected float TotalPrice { get; set; }
protected string PaymentError { get; set; }
protected override async Task OnInitializedAsync()
{
CartDrinks = await LocalStorage.GetItemAsync<List<DrinkViewModel>>(nameof(CartDrinks));
DiscountCodes = await LocalStorage.GetItemAsync<List<DiscountCodeViewModel>>(nameof(DiscountCodes)) ?? new List<DiscountCodeViewModel>();
TotalPrice = PriceService.GetTotalPrice(DiscountCodes, CartDrinks);
if (TotalPrice > 10f)
PaymentMethod = PaymentMethod.CreditCard;
}
protected async Task Pay()
{
PaymentError = default;
var paymentCart = new PurchaseCart();
paymentCart.Discounts = DiscountCodes;
paymentCart.PurchasingItems = CartDrinks.Select(cd => new CartItem
{
Id = cd.Id,
Quantity = cd.Quantity
}).ToList();
paymentCart.IsPayedWithCash = PaymentMethod == PaymentMethod.Cash;
if (!paymentCart.IsPayedWithCash)
{
paymentCart.CreditCardNumber = CreditCardForm.Number;
paymentCart.CreditCardExpirationMonth = CreditCardForm.ExpirationMonth;
paymentCart.CreditCardExpirationYear = CreditCardForm.ExpirationYear;
paymentCart.CreditCardCVVCode = CreditCardForm.CVVCode;
}
var paymentResult = await PaymentService.Pay(paymentCart);
if (!paymentResult.IsValid)
{
PaymentError = paymentResult.ErrorMessage;
return;
}
await LocalStorage.RemoveItemAsync(nameof(CartDrinks));
await LocalStorage.RemoveItemAsync(nameof(DiscountCodes));
Navigation.NavigateTo("done");
}
}
}

39
drinkMe/Client/Program.cs Normal file
View File

@ -0,0 +1,39 @@
using drinkMe.Client.Services;
using drinkMe.Client.Services.Interfaces;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Blazored.LocalStorage;
using System.Text.Json;
namespace drinkMe.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) })
.AddScoped<IDataService, DataService>()
.AddScoped<IPriceService, PriceService>()
.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddBlazoredLocalStorage(config =>
{
config.JsonSerializerOptions.WriteIndented = false;
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
await builder.Build().RunAsync();
}
}
}

View File

@ -0,0 +1,30 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:14962",
"sslPort": 44321
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"drinkMe": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,6 @@
@import "variables.scss";
@import "mixins.scss";
@import "framework-override.scss";
@import "neomorph.scss";
@import "base.scss";
@import "chooseDrinks.scss";

View File

@ -0,0 +1,41 @@
html {
scrollbar-width: thin;
}
*, ::after, ::before {
scrollbar-width: inherit;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-button {
width: 8px;
height: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
border: thin solid transparent;
box-shadow: none;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
border: thin solid transparent;
border-radius: 10px;
background: var(--primary-color);
}
body {
background-image: url("/images/background.jpg");
background-size: cover;
min-height: 100vh
}
.loading {
height: 100vh;
justify-content: center;
flex-direction: column;
}

View File

@ -0,0 +1,55 @@
.drinkCart {
&List {
display: flex;
flex-direction: column;
}
&Item {
display: grid;
grid-template-areas: "drinkName drinkQuantity drinkCross drinkPrice";
grid-template-columns: 1fr minmax(100px, 20%) 5% 10%;
grid-template-rows: 100%;
grid-gap: 5px;
align-items: center;
&:not(:last-child) {
margin-bottom: 1rem;
}
}
&PItem {
display: grid;
grid-template-areas: "drinkName drinkQuantity drinkCross drinkPrice";
grid-template-columns: 1fr 10% 5% 10%;
grid-template-rows: 100%;
grid-gap: 5px;
align-items: center;
&:not(:last-child) {
margin-bottom: 1rem;
}
}
&Name {
grid-area: drinkName;
}
&Cross {
grid-area: drinkCross;
}
&Quantity {
grid-area: drinkQuantity;
}
&Price {
grid-area: drinkPrice;
white-space: nowrap;
}
}
.column>.box>.title{
padding: 10%;
}

View File

@ -0,0 +1,16 @@
html {
overflow-y: auto;
}
.steps {
list-style-type: none !important;
}
@include MediaQuery(phone) {
.mt-2-mobile {
margin-top: .5rem !important;
}
}
.control.has-icons-left .icon, .control.has-icons-right .icon {
z-index: 5;
}

View File

@ -0,0 +1,47 @@
//MEDIA QUERY MANAGER
/*$breakpoint argument choices:
- phone
- tab-port
- tab-land
- desk
- big-desktop
*/
@mixin MediaQuery($breakpoint) {
@if $breakpoint==phone {
@media screen and (max-width: 768px) {
@content
}
}
@if $breakpoint==tab-p {
@media screen and (min-width: 768px) and (max-width: 900px) {
@content
}
}
@if $breakpoint==tab-l {
@media screen and (min-width: 901px) and (max-width: 1200px) {
@content
}
}
@if $breakpoint==laptop {
@media screen and (min-width: 1201px) and (max-width: 1366px) {
@content
}
}
@if $breakpoint==desk {
@media screen and (min-width: 1201px) and (max-width: 1800px) {
@content
}
}
@if $breakpoint==big-d {
@media screen and (min-width: 1801px) and (max-width: 4000px) {
@content
}
}
}

View File

@ -0,0 +1,155 @@
.neomorph {
box-shadow: -8px -8px 16px var(--light-shadow), 8px 8px 16px var(--dark-shadow);
&Small {
box-shadow: -6px -6px 12px var(--light-shadow), 6px 6px 12px var(--dark-shadow);
}
&XSmall {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow);
}
&XXSmall {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow);
}
&Bottom {
filter: drop-shadow(8px 8px 14px var(--primary-color-dark));
}
&Inset {
box-shadow: inset 8px 8px 16px var(--dark-shadow), inset -8px -8px 16px var(--light-shadow);
&Small {
box-shadow: inset 6px 6px 12px var(--dark-shadow), inset -6px -6px 12px var(--light-shadow);
}
&XSmall {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow);
}
&XXSmall {
box-shadow: inset 2px 2px 4px var(--dark-shadow), inset -2px -2px 4px var(--light-shadow);
}
}
}
.neo {
&Btn {
background: linear-gradient(145deg,var(--primary-gradiend-light),var(--primary-gradiend-dark));
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow);
border: none !important;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
&:focus {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
border: none !important;
}
&InsetPlain {
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light));
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow);
border: none !important;
&:focus {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow) !important;
border: none !important;
}
}
&Small {
background: linear-gradient(145deg,var(--primary-gradiend-light),var(--primary-gradiend-dark));
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow);
border: none !important;
&:focus {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow);
border: none !important;
&:not(:active) {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow);
}
}
&InsetPlain {
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light));
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow);
border: none !important;
&:focus {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow) !important;
border: none !important;
}
}
&XInsetPlain {
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light));
box-shadow: inset 2px 2px 4px var(--dark-shadow), inset -2px -2px 4px var(--light-shadow);
border: none !important;
&:focus {
box-shadow: inset 2px 2px 4px var(--dark-shadow), inset -2px -2px 4px var(--light-shadow) !important;
border: none !important;
}
}
&Plain {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow);
border: none !important;
&:focus {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
border: none !important;
}
}
}
}
&File {
box-shadow: 0px 0px 0px var(--light-shadow), 0px 0px 0px var(--dark-shadow) !important;
transition: all .2s linear;
-webkit-backface-visibility: hidden !important;
backface-visibility: hidden !important;
@include MediaQuery(phone) {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
background: var(--primary-color) !important;
}
&:hover {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
}
&.isSelected {
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light));
box-shadow: inset 3px 3px 6px var(--dark-shadow),inset -3px -3px 6px var(--light-shadow) !important;
}
&.is-active, &.active {
box-shadow: inset 3px 3px 6px var(--dark-shadow),inset -3px -3px 6px var(--light-shadow) !important;
color: var(--black) !important;
}
}
&Input {
box-shadow: inset 2px 2px 4px var(--dark-shadow),inset -2px -2px 4px var(--light-shadow) !important;
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light));
border: none !important;
&:focus {
border: none !important;
}
}
&Select > select {
box-shadow: inset 2px 2px 4px var(--dark-shadow),inset -2px -2px 4px var(--light-shadow) !important;
background: linear-gradient(145deg,var(--primary-gradiend-dark),var(--primary-gradiend-light)) !important;
border: none !important;
&:focus {
border: none !important;
}
}
}

View File

@ -0,0 +1,56 @@
$main-pc: hsl(25,84,88); //#fbe9d7;
$main-gradient-light: hsl(25,84,92);
$main-gradient-dark: hsl(25,84,84);
$main-light-pc: lighten($main-pc,15%);
$main-dark-pc: darken($main-pc,25%);
$main-sc: #f6d5f7;
$main-light-sc: lighten($main-sc,15%);
$main-dark-sc: darken($main-sc,15%);
$icon-primary: hsl(0,96,25);
$icon-secondary: hsl(0,100,55);
$text-color: darken($main-pc,70%);
$black: hsl(0, 0%, 4%) !default;
$black-bis: hsl(0, 0%, 7%) !default;
$black-ter: hsl(0, 0%, 14%) !default;
$grey-darker: hsl(0, 0%, 21%) !default;
$grey-dark: hsl(0, 0%, 29%) !default;
$grey: hsl(0, 0%, 48%) !default;
$grey-light: hsl(0, 0%, 71%) !default;
$grey-lighter: hsl(0, 0%, 86%) !default;
$white-ter: hsl(0, 4%, 96%) !default;
$white-bis: hsl(0, 4%, 98%) !default;
$white: hsl(0, 4%, 99.8%) !default;
$danger: hsl(0, 82%, 91%);
$warning: hsl(48, 82%, 91%);
$info: hsl(196, 82%, 91%);
:root {
--background: $main-pc;
--text-color: $text-color;
--black: $black;
--black-bis: $black-bis;
--black-ter: $black-ter;
--white: $white;
--primary-color: $main-pc;
--primary-color-light: $main-light-pc;
--primary-color-dark: $main-dark-pc;
--primary-gradiend-light: $main-gradient-light;
--primary-gradiend-dark: $main-gradient-dark;
--secondary-color: $main-sc;
--secondary-color-light: $main-light-sc;
--secondary-color-dark: $main-dark-sc;
--light-shadow: rgba($main-light-pc, .5);
--dark-shadow: rgba($main-dark-pc, .5);
/* --fa-primary-color: $text-color;
--fa-secondary-color: $main-dark-pc;
--fa-primary-opacity: 0.80;
--fa-secondary-opacity: 0.80;*/
--danger-color: $danger;
--warning-color: $warning;
--info-color: $info;
}

View File

@ -0,0 +1,47 @@
using collAnon.Pub.Shared;
using drinkMe.Client.Services.Interfaces;
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace drinkMe.Client.Services
{
public class DataService : IDataService
{
readonly HttpClient HttpClient;
public DataService(HttpClient httpClient)
{
HttpClient = httpClient;
}
public async Task<WebResult> IsValidDiscountCode(string code)
{
var result = new WebResult();
var httpResponse = await HttpClient.GetAsync($"api/Drinks/{nameof(IsValidDiscountCode)}?{nameof(code)}={code}");
if (!httpResponse.IsSuccessStatusCode)
return result.Invalidate(JsonSerializer.Deserialize<WebResult>(await httpResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
result.Data = JsonSerializer.Deserialize<DiscountCodeViewModel>(await httpResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
return result;
}
public async Task<WebResult> GetDrinks()
{
var result = new WebResult();
var httpResponse = await HttpClient.GetAsync($"api/Drinks/{nameof(GetDrinks)}");
if (!httpResponse.IsSuccessStatusCode)
return result.Invalidate(JsonSerializer.Deserialize<WebResult>(await httpResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
result.Data = JsonSerializer.Deserialize<List<DrinkViewModel>>(await httpResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
return result;
}
}
}

View File

@ -0,0 +1,11 @@
using collAnon.Pub.Shared;
using System.Threading.Tasks;
namespace drinkMe.Client.Services.Interfaces
{
public interface IDataService
{
Task<WebResult> GetDrinks();
Task<WebResult> IsValidDiscountCode(string code);
}
}

View File

@ -0,0 +1,14 @@
using collAnon.Pub.Shared;
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Client.Services.Interfaces
{
public interface IPaymentService
{
Task<WebResult> Pay(PurchaseCart purchaseCart);
}
}

View File

@ -0,0 +1,13 @@
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Client.Services.Interfaces
{
public interface IPriceService
{
float GetTotalPrice(List<DiscountCodeViewModel> discountCodes, List<DrinkViewModel> drinks);
}
}

View File

@ -0,0 +1,33 @@
using collAnon.Pub.Shared;
using drinkMe.Client.Services.Interfaces;
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
namespace drinkMe.Client.Services
{
public class PaymentService : IPaymentService
{
readonly HttpClient HttpClient;
public PaymentService(HttpClient httpClient)
{
HttpClient = httpClient;
}
public async Task<WebResult> Pay(PurchaseCart purchaseCart)
{
var result = new WebResult();
var httpResponse = await HttpClient.PostAsync($"api/Drinks/{nameof(Pay)}", JsonContent.Create(purchaseCart));
if (!httpResponse.IsSuccessStatusCode)
return result.Invalidate(JsonSerializer.Deserialize<WebResult>(await httpResponse.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
return result;
}
}
}

View File

@ -0,0 +1,34 @@
using Blazored.LocalStorage;
using drinkMe.Client.Services.Interfaces;
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Client.Services
{
public class PriceService : IPriceService
{
public float GetTotalPrice(List<DiscountCodeViewModel> discountCodes, List<DrinkViewModel> cartDrinks)
{
var total = default(float);
if (discountCodes.Count == 0)
foreach (var cartDrink in cartDrinks)
total += cartDrink.Price * cartDrink.Quantity;
else
foreach (var cartDrink in cartDrinks)
if (discountCodes.Any(d => d.ApplicableProducts.Contains(cartDrink.Id)))
{
var discountPercentage = discountCodes.First(d => d.ApplicableProducts.Contains(cartDrink.Id)).DiscountPercentage / 100;
var discountedPricePortion = cartDrink.Price * discountPercentage;
total += (cartDrink.Price - discountedPricePortion) * cartDrink.Quantity;
}
else
total += cartDrink.Price * cartDrink.Quantity;
return total;
}
}
}

View File

@ -0,0 +1,10 @@
@inherits LayoutComponentBase
<NavMenu />
<section class="section">
<div class="container">
@Body
</div>
</section>

View File

@ -0,0 +1,37 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<NavLink class="navbar-item" href="/">
<img src="/images/drinkMe-logo.png" alt="logo">
</NavLink>
<a role="button" class="navbar-burger @IsActive" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample"
@onclick="OpenCloseMenu">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu @IsActive">
<div class="navbar-start">
<NavLink class="navbar-item" href="/">
<span class="icon-text">
<span class="icon"><i class="fad fa-home"></i></span>
<span>Home</span>
</span>
</NavLink>
</div>
</div>
</nav>
@code{
string IsActive { get; set; }
void OpenCloseMenu()
{
if (string.IsNullOrEmpty(IsActive))
IsActive = "is-active";
else
IsActive = default;
}
}

View File

@ -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 Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using drinkMe.Client
@using drinkMe.Client.Models
@using drinkMe.Client.Helpers
@using drinkMe.Client.Components
@using drinkMe.Client.Shared

View File

@ -0,0 +1,6 @@
[
{
"outputFile": "wwwroot/css/style.css",
"inputFile": "SCSS/all.scss"
}
]

View File

@ -0,0 +1,63 @@
{
"compilers": {
"less": {
"autoPrefix": "",
"cssComb": "none",
"ieCompat": true,
"strictMath": false,
"strictUnits": false,
"relativeUrls": true,
"rootPath": "",
"sourceMapRoot": "",
"sourceMapBasePath": "",
"sourceMap": false
},
"sass": {
"autoPrefix": "",
"includePath": "",
"indentType": "space",
"indentWidth": 2,
"outputStyle": "nested",
"Precision": 5,
"relativeUrls": true,
"sourceMapRoot": "",
"lineFeed": "",
"sourceMap": false
},
"stylus": {
"sourceMap": false
},
"babel": {
"sourceMap": false
},
"coffeescript": {
"bare": false,
"runtimeMode": "node",
"sourceMap": false
},
"handlebars": {
"root": "",
"noBOM": false,
"name": "",
"namespace": "",
"knownHelpersOnly": false,
"forcePartial": false,
"knownHelpers": [],
"commonjs": "",
"amd": false,
"sourceMap": false
}
},
"minifiers": {
"css": {
"enabled": true,
"termSemicolons": true,
"gzip": false
},
"javascript": {
"enabled": true,
"termSemicolons": true,
"gzip": false
}
}
}

View File

@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup>
<Content Remove="compilerconfig.json" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\css\style.css" />
</ItemGroup>
<ItemGroup>
<None Include="compilerconfig.json" />
<None Include="wwwroot\webfonts\fa-duotone-900.svg" />
<None Include="wwwroot\webfonts\fa-duotone-900.woff2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="3.0.0" />
<PackageReference Include="Blazored.Modal" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="5.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="5.0.5" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\drinkMe.Client.Models\drinkMe.Client.Models.csproj" />
<ProjectReference Include="..\Shared\drinkMe.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\images\drinks\" />
</ItemGroup>
</Project>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11705
drinkMe/Client/wwwroot/css/bulma.css vendored Normal file

File diff suppressed because it is too large Load Diff