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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7134
drinkMe/Client/wwwroot/css/fontawesome.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,215 @@
:root {
--background: #fadcc7;
--text-color: #542707;
--black: #0a0a0a;
--black-bis: #121212;
--black-ter: #242424;
--white: #fffefe;
--primary-color: #fadcc7;
--primary-color-light: white;
--primary-color-dark: #f09351;
--primary-gradiend-light: #fce8d9;
--primary-gradiend-dark: #f8d0b4;
--secondary-color: #f6d5f7;
--secondary-color-light: white;
--secondary-color-dark: #e895eb;
--light-shadow: rgba(255, 255, 255, 0.5);
--dark-shadow: rgba(240, 147, 81, 0.5);
/* --fa-primary-color: $text-color;
--fa-secondary-color: $main-dark-pc;
--fa-primary-opacity: 0.80;
--fa-secondary-opacity: 0.80;*/
--danger-color: #fbd5d5;
--warning-color: #fbf3d5;
--info-color: #d5f1fb; }
/*$breakpoint argument choices:
- phone
- tab-port
- tab-land
- desk
- big-desktop
*/
html {
overflow-y: auto; }
.steps {
list-style-type: none !important; }
@media screen and (max-width: 768px) {
.mt-2-mobile {
margin-top: .5rem !important; } }
.control.has-icons-left .icon, .control.has-icons-right .icon {
z-index: 5; }
.neomorph {
box-shadow: -8px -8px 16px var(--light-shadow), 8px 8px 16px var(--dark-shadow); }
.neomorphSmall {
box-shadow: -6px -6px 12px var(--light-shadow), 6px 6px 12px var(--dark-shadow); }
.neomorphXSmall {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow); }
.neomorphXXSmall {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow); }
.neomorphBottom {
filter: drop-shadow(8px 8px 14px var(--primary-color-dark)); }
.neomorphInset {
box-shadow: inset 8px 8px 16px var(--dark-shadow), inset -8px -8px 16px var(--light-shadow); }
.neomorphInsetSmall {
box-shadow: inset 6px 6px 12px var(--dark-shadow), inset -6px -6px 12px var(--light-shadow); }
.neomorphInsetXSmall {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow); }
.neomorphInsetXXSmall {
box-shadow: inset 2px 2px 4px var(--dark-shadow), inset -2px -2px 4px var(--light-shadow); }
.neoBtn {
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; }
.neoBtn:focus {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
border: none !important; }
.neoBtnInsetPlain {
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; }
.neoBtnInsetPlain:focus {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow) !important;
border: none !important; }
.neoBtnSmall {
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; }
.neoBtnSmall:focus {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow);
border: none !important; }
.neoBtnSmall:focus:not(:active) {
box-shadow: -2px -2px 4px var(--light-shadow), 2px 2px 4px var(--dark-shadow); }
.neoBtnSmallInsetPlain {
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; }
.neoBtnSmallInsetPlain:focus {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow) !important;
border: none !important; }
.neoBtnSmallXInsetPlain {
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; }
.neoBtnSmallXInsetPlain:focus {
box-shadow: inset 2px 2px 4px var(--dark-shadow), inset -2px -2px 4px var(--light-shadow) !important;
border: none !important; }
.neoBtnSmallPlain {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow);
border: none !important; }
.neoBtnSmallPlain:focus {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
border: none !important; }
.neoFile {
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; }
@media screen and (max-width: 768px) {
.neoFile {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important;
background: var(--primary-color) !important; } }
.neoFile:hover {
box-shadow: -3px -3px 6px var(--light-shadow), 3px 3px 6px var(--dark-shadow) !important; }
.neoFile.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; }
.neoFile.is-active, .neoFile.active {
box-shadow: inset 3px 3px 6px var(--dark-shadow), inset -3px -3px 6px var(--light-shadow) !important;
color: var(--black) !important; }
.neoInput {
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; }
.neoInput:focus {
border: none !important; }
.neoSelect > 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; }
.neoSelect > select:focus {
border: none !important; }
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; }
.drinkCartList {
display: flex;
flex-direction: column; }
.drinkCartItem {
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; }
.drinkCartItem:not(:last-child) {
margin-bottom: 1rem; }
.drinkCartPItem {
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; }
.drinkCartPItem:not(:last-child) {
margin-bottom: 1rem; }
.drinkCartName {
grid-area: drinkName; }
.drinkCartCross {
grid-area: drinkCross; }
.drinkCartQuantity {
grid-area: drinkQuantity; }
.drinkCartPrice {
grid-area: drinkPrice;
white-space: nowrap; }
.column > .box > .title {
padding: 10%; }

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>drinkMe</title>
<base href="/" />
<link href="css/bulma.min.css" rel="stylesheet" />
<link href="css/bulma-steps.min.css" rel="stylesheet" />
<link href="css/bulma-divider.min.css" rel="stylesheet" />
<link href="css/fontawesome.min.css" rel="stylesheet" />
<link href="css/duotone.min.css" rel="stylesheet" />
<link href="css/style.min.css" rel="stylesheet" />
<link href="manifest.json" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
</head>
<body>
<div id="app">
<div class="is-flex is-align-items-center loading">
<p><img src="images/drinkMe-logo.png" alt="loading logo" /></p>
<h1 class="title">Loading...</h1>
</div>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"name": "drinkMe",
"short_name": "drinkMe",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@ -0,0 +1,4 @@
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });

View File

@ -0,0 +1,48 @@
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate';
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,95 @@
using collAnon.Pub.Shared;
using drinkMe.Server.Models;
using drinkMe.Server.Services.Interfaces;
using drinkMe.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Server.Controllers
{
[ApiController]
[Route("api/[controller]/[action]")]
public class DrinksController : ControllerBase
{
readonly IDataService DataService;
readonly IPaymentService PaymentService;
public DrinksController(IDataService dataService, IPaymentService paymentService)
{
DataService = dataService;
PaymentService = paymentService;
}
[HttpGet]
public async Task<IActionResult> IsValidDiscountCode([FromQuery] string code)
{
try
{
var validationResult = await DataService.IsValidDiscountCode(code);
if (!validationResult.IsValid)
return StatusCode(validationResult.StatusCode, validationResult);
var validationData = validationResult.Data as DiscountCode;
var resultData = new DiscountCodeViewModel
{
Code = validationData.Code,
ApplicableProducts = validationData.ApplicableProducts,
DiscountPercentage = validationData.DiscountPercentage
};
return Ok(resultData);
}
catch (Exception ex)
{
return BadRequest(new WebResult().Invalidate($"Error at ${nameof(IsValidDiscountCode)}", default, ex));
}
}
[HttpGet]
public async Task<IActionResult> GetDrinks()
{
try
{
var drinks = DataService.GetDrinks();
if (drinks.Count == 0)
return NotFound(new WebResult().Invalidate("No drinks available.", StatusCodes.Status404NotFound));
var viewDrinks = drinks.Select(d => new DrinkViewModel
{
Id = d.Id,
Name = d.Name,
Description = d.Description,
Price = d.Price,
PictureName = d.PictureName
}).ToArray();
return Ok(viewDrinks);
}
catch (Exception ex)
{
return BadRequest(new WebResult().Invalidate($"Error at ${nameof(GetDrinks)}", default, ex));
}
}
[HttpPost]
public async Task<IActionResult> Pay(PurchaseCart purchaseCart)
{
try
{
var successfulPayment = await PaymentService.Pay(purchaseCart);
if (!successfulPayment)
return BadRequest(new WebResult().Invalidate("Payment failed.", StatusCodes.Status400BadRequest));
return Ok();
}
catch (Exception ex)
{
return BadRequest(new WebResult().Invalidate($"Error at ${nameof(Pay)}", default, ex));
}
}
}
}

26
drinkMe/Server/Program.cs Normal file
View File

@ -0,0 +1,26 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Server
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

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.Server": {
"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,69 @@
using collAnon.Pub.Shared;
using drinkMe.Server.Models;
using drinkMe.Server.Services.Interfaces;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Server.Services
{
public class DataService : IDataService
{
List<DiscountCode> DiscountCodes = new List<DiscountCode>
{
new DiscountCode{ Code ="ITS_M0NDAY", DiscountPercentage = 15, ApplicableProducts = new[] { 2 } },
new DiscountCode{ Code ="ITS_K1NDA_COLD", DiscountPercentage = 30, ApplicableProducts = new[] { 4 } },
new DiscountCode{ Code ="I_NEED_R3LAX", DiscountPercentage = 25, ApplicableProducts = new[] { 3 } },
new DiscountCode{ Code ="D1AMOND_HANDS", DiscountPercentage = 100, ApplicableProducts = new[] { 1 } }
};
public List<Drink> GetDrinks() =>
new List<Drink>
{
new Drink
{
Id = 1,
Name = "Italian Coffee",
Description = "Short but strong.",
Price = 3.50f,
PictureName = "italian-coffee.jpg"
},
new Drink
{
Id = 2,
Name = "American Coffee",
Description = "Dilluted in water to a cup size, not as strong as the italian one.",
Price = 3.0f,
PictureName = "american-coffee.jpg"
},
new Drink
{
Id = 3,
Name = "Tea",
Description = "Black Earl Grey, good alternative to coffee from time to time.",
Price = 2.0f,
PictureName = "tea.png"
},
new Drink
{
Id = 4,
Name = "Chocolate",
Description = "Your dose of sugar and calories, just don't loose the hang.",
Price = 4.0f,
PictureName = "chocolate.jpg"
}
};
public async Task<WebResult> IsValidDiscountCode(string code)
{
var result = new WebResult();
if (!DiscountCodes.Any(dc => dc.Code == code))
return result.Invalidate("Invalid discount code.", StatusCodes.Status404NotFound);
result.Data = DiscountCodes.First(dc => dc.Code == code);
return result;
}
}
}

View File

@ -0,0 +1,13 @@
using collAnon.Pub.Shared;
using drinkMe.Server.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace drinkMe.Server.Services.Interfaces
{
public interface IDataService
{
List<Drink> GetDrinks();
Task<WebResult> IsValidDiscountCode(string code);
}
}

View File

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

View File

@ -0,0 +1,18 @@
using drinkMe.Server.Services.Interfaces;
using drinkMe.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace drinkMe.Server.Services
{
public class PaymentService : IPaymentService
{
public async Task<bool> Pay(PurchaseCart purchaseCart)
{
//completing the purchase
return true;
}
}
}

66
drinkMe/Server/Startup.cs Normal file
View File

@ -0,0 +1,66 @@
using drinkMe.Server.Services;
using drinkMe.Server.Services.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Linq;
namespace drinkMe.Server
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IDataService, DataService>()
.AddScoped<IPaymentService, PaymentService>();
services.AddControllers().AddJsonOptions(options =>
{
options.JsonSerializerOptions.IgnoreReadOnlyFields = true;
options.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
});
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
// 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.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}

View File

@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="5.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Client\drinkMe.Client.csproj" />
<ProjectReference Include="..\drinkMe.Server.Models\drinkMe.Server.Models.csproj" />
<ProjectReference Include="..\Shared\drinkMe.Shared.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace drinkMe.Shared
{
public class CartItem
{
public int Id { get; set; }
public int Quantity { get; set; } = 1;
}
}

View File

@ -0,0 +1,9 @@
namespace drinkMe.Shared
{
public class DiscountCodeViewModel
{
public string Code { get; set; }
public float DiscountPercentage { get; set; }
public int[] ApplicableProducts { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace drinkMe.Shared
{
public class DrinkViewModel
{
public int Id { get; set; }
public string Name { get; set; } = "Missing name";
public string Description { get; set; } = "Missing description";
public string PictureName { get; set; } = "missing.png";
[Range(1, 20)]
public int Quantity { get; set; } = 1;
public float Price { get; set; } = 0;
public bool IsInTheCart { get; set; } = false;
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace drinkMe.Shared
{
public class PurchaseCart
{
public bool IsPayedWithCash { get; set; } = false;
public List<CartItem> PurchasingItems { get; set; } = new List<CartItem>();
public List<DiscountCodeViewModel> Discounts { get; set; } = new List<DiscountCodeViewModel>();
public string CreditCardNumber { get; set; }
public string CreditCardExpirationMonth { get; set; }
public string CreditCardExpirationYear { get; set; }
public string CreditCardCVVCode { get; set; }
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Text.Json.Serialization;
namespace collAnon.Pub.Shared
{
public class WebResult
{
public WebResult Invalidate<T>(string errorMessage, int statusCode = StatusCodes.Status400BadRequest, Exception exception = null, T defaultData = default)
{
IsValid = false;
ErrorMessage = errorMessage;
StatusCode = statusCode;
Exception = exception;
Data = defaultData;
return this;
}
public WebResult Invalidate(string errorMessage, int statusCode = StatusCodes.Status400BadRequest, Exception exception = null)
{
IsValid = false;
ErrorMessage = errorMessage;
StatusCode = statusCode;
Exception = exception;
return this;
}
public WebResult Invalidate(WebResult result)
{
IsValid = result.IsValid;
ErrorMessage = result.ErrorMessage;
StatusCode = result.StatusCode;
Exception = result.Exception;
Data = result.Data;
return this;
}
public int StatusCode { get; set; } = StatusCodes.Status200OK;
public string ErrorMessage { get; set; }
public bool IsValid { get; set; } = true;
[JsonIgnore]
public object Data { get; set; }
[JsonIgnore]
public Exception Exception { get; set; }
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace drinkMe.Client.Models
{
public class CreditCardForm
{
[Required, Display(Name = "Credit card number"),
MaxLength(16), MinLength(16)]
public string Number { get; set; }
[Required, Display(Name = "Expiration month"),
Range(2, 2), RegularExpression(@"^((0[1-9])|(1[0-2]))$", ErrorMessage = "Invalid month")]
public string ExpirationMonth { get; set; }
[Required, Display(Name = "Expiration year"),
Range(2, 2), RegularExpression(@"^(\d{2})$", ErrorMessage = "Invalid year")]
public string ExpirationYear { get; set; }
[Required, Display(Name = "CVV code"),
Range(3, 3), RegularExpression(@"^(\d{3})$", ErrorMessage = "Invalid CVV code")]
public string CVVCode { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace drinkMe.Client.Models
{
public class DiscountForm
{
[Required,
Display(Name = "Discount code")]
public string DiscountCode { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace drinkMe.Client.Models
{
public enum PaymentMethod
{
Undefined,
Cash,
CreditCard
}
}

View File

@ -0,0 +1,9 @@
namespace drinkMe.Client.Models
{
public enum PurchaseStep
{
ChoosingProducts,
PaymentSetup,
SuccessfulPurchase
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
namespace drinkMe.Server.Models
{
public class DiscountCode
{
public string Code { get; set; }
public float DiscountPercentage { get; set; }
public int[] ApplicableProducts { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace drinkMe.Server.Models
{
public class Drink
{
public int Id { get; set; }
public string Name { get; set; } = "Missing name";
public string Description { get; set; } = "Missing description";
public float Price { get; set; } = 0;
public string PictureName { get; set; } = "missing.png";
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>