initial commit
435
.gitignore
vendored
Normal file
@ -0,0 +1,435 @@
|
||||
*.db
|
||||
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- Backup*.rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# Mac bundle stuff
|
||||
*.dmg
|
||||
*.app
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
##
|
||||
## Visual Studio Code
|
||||
##
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
13
BlazingComponents/BlazingComponents.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<RazorLangVersion>3.0</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(AspNetCoreVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
13
BlazingComponents/TemplatedDialog.razor
Normal file
@ -0,0 +1,13 @@
|
||||
@if (Show)
|
||||
{
|
||||
<div class="dialog-container">
|
||||
<div class="dialog">
|
||||
@ChildContent
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment ChildContent { get; set; }
|
||||
[Parameter] public bool Show { get; set; }
|
||||
}
|
36
BlazingComponents/TemplatedList.razor
Normal file
@ -0,0 +1,36 @@
|
||||
@typeparam TItem
|
||||
|
||||
@if (items == null)
|
||||
{
|
||||
@Loading
|
||||
}
|
||||
else if (!items.Any())
|
||||
{
|
||||
@Empty
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="list-group @ListGroupClass">
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<div class="list-group-item">
|
||||
@Item(item)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
IEnumerable<TItem> items;
|
||||
|
||||
[Parameter] public Func<Task<IEnumerable<TItem>>> Loader { get; set; }
|
||||
[Parameter] public RenderFragment Loading { get; set; }
|
||||
[Parameter] public RenderFragment Empty { get; set; }
|
||||
[Parameter] public RenderFragment<TItem> Item { get; set; }
|
||||
[Parameter] public string ListGroupClass { get; set; }
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
items = await Loader();
|
||||
}
|
||||
}
|
1
BlazingComponents/_Imports.razor
Normal file
@ -0,0 +1 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
19
BlazingPizza.Client/App.razor
Normal file
@ -0,0 +1,19 @@
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="typeof(Program).Assembly" Context="routeData">
|
||||
<Found>
|
||||
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin />
|
||||
</NotAuthorized>
|
||||
<Authorizing>
|
||||
<div class="main">Bitte warten...</div>
|
||||
</Authorizing>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="typeof(MainLayout)">
|
||||
<div class="main">Nichts gefunden</div>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
||||
</CascadingAuthenticationState>
|
23
BlazingPizza.Client/BlazingPizza.Client.csproj
Normal file
@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<RazorLangVersion>3.0</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(BlazorVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(BlazorVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Build" Version="$(BlazorVersion)" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="System.Net.Http.Json" Version="$(SystemNetHttpJsonVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BlazingComponents\BlazingComponents.csproj" />
|
||||
<ProjectReference Include="..\BlazingPizza.ComponentsLibrary\BlazingPizza.ComponentsLibrary.csproj" />
|
||||
<ProjectReference Include="..\BlazingPizza.Shared\BlazingPizza.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
13
BlazingPizza.Client/JSRuntimeExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Microsoft.JSInterop;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazingPizza.Client
|
||||
{
|
||||
public static class JSRuntimeExtensions
|
||||
{
|
||||
public static ValueTask<bool> Confirm(this IJSRuntime jsRuntime, string message)
|
||||
{
|
||||
return jsRuntime.InvokeAsync<bool>("confirm", message);
|
||||
}
|
||||
}
|
||||
}
|
55
BlazingPizza.Client/OrderState.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BlazingPizza.Client
|
||||
{
|
||||
public class OrderState
|
||||
{
|
||||
public bool ShowingConfigureDialog { get; private set; }
|
||||
|
||||
public Pizza ConfiguringPizza { get; private set; }
|
||||
|
||||
public Order Order { get; private set; } = new Order();
|
||||
|
||||
public void ShowConfigurePizzaDialog(PizzaSpecial special)
|
||||
{
|
||||
ConfiguringPizza = new Pizza()
|
||||
{
|
||||
Special = special,
|
||||
SpecialId = special.Id,
|
||||
Size = Pizza.DefaultSize,
|
||||
Toppings = new List<PizzaTopping>(),
|
||||
};
|
||||
|
||||
ShowingConfigureDialog = true;
|
||||
}
|
||||
|
||||
public void CancelConfigurePizzaDialog()
|
||||
{
|
||||
ConfiguringPizza = null;
|
||||
ShowingConfigureDialog = false;
|
||||
}
|
||||
|
||||
public void ConfirmConfigurePizzaDialog()
|
||||
{
|
||||
Order.Pizzas.Add(ConfiguringPizza);
|
||||
ConfiguringPizza = null;
|
||||
|
||||
ShowingConfigureDialog = false;
|
||||
}
|
||||
|
||||
public void RemoveConfiguredPizza(Pizza pizza)
|
||||
{
|
||||
Order.Pizzas.Remove(pizza);
|
||||
}
|
||||
|
||||
public void ResetOrder()
|
||||
{
|
||||
Order = new Order();
|
||||
}
|
||||
|
||||
public void ReplaceOrder(Order order)
|
||||
{
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
}
|
41
BlazingPizza.Client/OrdersClient.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazingPizza.Client
|
||||
{
|
||||
public class OrdersClient
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public OrdersClient(HttpClient httpClient)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrderWithStatus>> GetOrders() =>
|
||||
await httpClient.GetFromJsonAsync<IEnumerable<OrderWithStatus>>("orders");
|
||||
|
||||
|
||||
public async Task<OrderWithStatus> GetOrder(int orderId) =>
|
||||
await httpClient.GetFromJsonAsync<OrderWithStatus>($"orders/{orderId}");
|
||||
|
||||
|
||||
public async Task<int> PlaceOrder(Order order)
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync("orders", order);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var orderId = await response.Content.ReadFromJsonAsync<int>();
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public async Task SubscribeToNotifications(NotificationSubscription subscription)
|
||||
{
|
||||
var response = await httpClient.PutAsJsonAsync("notifications/subscribe", subscription);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
32
BlazingPizza.Client/Pages/Authentication.razor
Normal file
@ -0,0 +1,32 @@
|
||||
@page "/authentication/{action}"
|
||||
@inject OrderState OrderState
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<RemoteAuthenticatorViewCore
|
||||
TAuthenticationState="PizzaAuthenticationState"
|
||||
AuthenticationState="RemoteAuthenticationState"
|
||||
OnLogInSucceeded="RestorePizza"
|
||||
Action="@Action" />
|
||||
|
||||
@code{
|
||||
[Parameter] public string Action { get; set; }
|
||||
|
||||
public PizzaAuthenticationState RemoteAuthenticationState { get; set; } = new PizzaAuthenticationState();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (RemoteAuthenticationActions.IsAction(RemoteAuthenticationActions.LogIn, Action))
|
||||
{
|
||||
// Preserve the current order so that we don't loose it
|
||||
RemoteAuthenticationState.Order = OrderState.Order;
|
||||
}
|
||||
}
|
||||
|
||||
private void RestorePizza(PizzaAuthenticationState pizzaState)
|
||||
{
|
||||
if (pizzaState.Order != null)
|
||||
{
|
||||
OrderState.ReplaceOrder(pizzaState.Order);
|
||||
}
|
||||
}
|
||||
}
|
70
BlazingPizza.Client/Pages/Checkout.razor
Normal file
@ -0,0 +1,70 @@
|
||||
@page "/checkout"
|
||||
@attribute [Authorize]
|
||||
@inject OrderState OrderState
|
||||
@inject OrdersClient OrdersClient
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div class="main">
|
||||
<EditForm Model="OrderState.Order.DeliveryAddress" OnValidSubmit="PlaceOrder">
|
||||
<div class="checkout-cols">
|
||||
<div class="checkout-order-details">
|
||||
<h4>Bestellung überprüfen</h4>
|
||||
<OrderReview Order="OrderState.Order" />
|
||||
</div>
|
||||
|
||||
<div class="checkout-delivery-address">
|
||||
<h4>Lieferaddresse...</h4>
|
||||
<AddressEditor Address="OrderState.Order.DeliveryAddress" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="checkout-button btn btn-warning" disabled="@isSubmitting">
|
||||
Bestellen
|
||||
</button>
|
||||
|
||||
<DataAnnotationsValidator />
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
bool isSubmitting;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// In the background, ask if they want to be notified about order updates
|
||||
_ = RequestNotificationSubscriptionAsync();
|
||||
}
|
||||
|
||||
async Task RequestNotificationSubscriptionAsync()
|
||||
{
|
||||
var subscription = await JSRuntime.InvokeAsync<NotificationSubscription>("blazorPushNotifications.requestSubscription");
|
||||
if (subscription != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await OrdersClient.SubscribeToNotifications(subscription);
|
||||
}
|
||||
catch (AccessTokenNotAvailableException ex)
|
||||
{
|
||||
ex.Redirect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async Task PlaceOrder()
|
||||
{
|
||||
isSubmitting = true;
|
||||
|
||||
try
|
||||
{
|
||||
var newOrderId = await OrdersClient.PlaceOrder(OrderState.Order);
|
||||
OrderState.ResetOrder();
|
||||
NavigationManager.NavigateTo($"myorders/{newOrderId}");
|
||||
}
|
||||
catch (AccessTokenNotAvailableException ex)
|
||||
{
|
||||
ex.Redirect();
|
||||
}
|
||||
}
|
||||
}
|
24
BlazingPizza.Client/Pages/Impressum.razor
Normal file
@ -0,0 +1,24 @@
|
||||
@page "/impressum"
|
||||
|
||||
<div class="main">
|
||||
<h1>Impressum</h1>
|
||||
<p>Angaben gemäß § 5 TMG</p>
|
||||
<p>Max Muster <br>
|
||||
Musterweg<br>
|
||||
12345 Musterstadt <br>
|
||||
</p>
|
||||
<p> <strong>Vertreten durch: </strong><br>
|
||||
Max Muster<br>
|
||||
</p>
|
||||
<p><strong>Kontakt:</strong> <br>
|
||||
Telefon: 01234-789456<br>
|
||||
Fax: 1234-56789<br>
|
||||
E-Mail: <a href='mailto:max@muster.de'>max@muster.de</a><br></p>
|
||||
<p><strong>Umsatzsteuer-ID: </strong> <br>
|
||||
Umsatzsteuer-Identifikationsnummer gemäß §27a Umsatzsteuergesetz: Musterustid.<br><br>
|
||||
<strong>Wirtschafts-ID: </strong><br>
|
||||
Musterwirtschaftsid<br>
|
||||
</p>
|
||||
<p><strong>Aufsichtsbehörde:</strong><br>
|
||||
Musteraufsicht Musterstadt<br></p><br>
|
||||
</div>
|
74
BlazingPizza.Client/Pages/Index.razor
Normal file
@ -0,0 +1,74 @@
|
||||
@page "/"
|
||||
@inject HttpClient HttpClient
|
||||
@inject OrderState OrderState
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<div class="main">
|
||||
<ul class="pizza-cards">
|
||||
@if (specials != null)
|
||||
{
|
||||
@foreach (var special in specials)
|
||||
{
|
||||
<li @onclick="@(() =>OrderState.ShowConfigurePizzaDialog(special))" style="background-image: url('@special.ImageUrl')">
|
||||
<div class="pizza-info">
|
||||
<span class="title">@special.Name</span>
|
||||
@special.Description
|
||||
<span class="price">@special.GetFormattedBasePrice()</span>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
@if (Order.Pizzas.Any())
|
||||
{
|
||||
<div class="order-contents">
|
||||
<h2>Ihre Bestellung</h2>
|
||||
|
||||
@foreach (var configuredPizza in Order.Pizzas)
|
||||
{
|
||||
<ConfiguredPizzaItem Pizza="configuredPizza" OnRemoved="@(() => RemovePizza(configuredPizza))" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-cart">Wählen sie eine Pizza<br>um zu beginnen</div>
|
||||
}
|
||||
|
||||
<div class="order-total @(Order.Pizzas.Any() ? "" : "hidden")">
|
||||
Summe:
|
||||
<span class="total-price">@Order.GetFormattedTotalPrice()</span>
|
||||
<a href="checkout" class="@(Order.Pizzas.Count == 0 ? "btn btn-warning disabled" : "btn btn-warning")">
|
||||
Bestellen >
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TemplatedDialog Show="OrderState.ShowingConfigureDialog">
|
||||
<ConfigurePizzaDialog
|
||||
Pizza="OrderState.ConfiguringPizza"
|
||||
OnCancel="OrderState.CancelConfigurePizzaDialog"
|
||||
OnConfirm="OrderState.ConfirmConfigurePizzaDialog" />
|
||||
</TemplatedDialog>
|
||||
|
||||
@code {
|
||||
List<PizzaSpecial> specials;
|
||||
Order Order => OrderState.Order;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
specials = await HttpClient.GetFromJsonAsync<List<PizzaSpecial>>("specials");
|
||||
}
|
||||
|
||||
async Task RemovePizza(Pizza configuredPizza)
|
||||
{
|
||||
if (await JS.Confirm($"{configuredPizza.Special.Name} von der Bestellung entfernen?"))
|
||||
{
|
||||
OrderState.RemoveConfiguredPizza(configuredPizza);
|
||||
}
|
||||
}
|
||||
}
|
46
BlazingPizza.Client/Pages/MyOrders.razor
Normal file
@ -0,0 +1,46 @@
|
||||
@page "/myorders"
|
||||
@attribute [Authorize]
|
||||
@inject OrdersClient OrdersClient
|
||||
|
||||
<div class="main">
|
||||
<TemplatedList Loader="@LoadOrders" ListGroupClass="orders-list">
|
||||
<Loading>Lade...</Loading>
|
||||
<Empty>
|
||||
<h2>Noch keine Bestellungen</h2>
|
||||
<a class="btn btn-success" href="">Bestell eine Pizza</a>
|
||||
</Empty>
|
||||
<Item Context="item">
|
||||
<div class="col">
|
||||
<h5>@item.Order.CreatedTime.ToLongDateString()</h5>
|
||||
Produkte:
|
||||
<strong>@item.Order.Pizzas.Count()</strong>;
|
||||
Gesamtpreis:
|
||||
<strong>€@item.Order.GetFormattedTotalPrice()</strong>
|
||||
</div>
|
||||
<div class="col">
|
||||
Status: <strong>@item.StatusText</strong>
|
||||
</div>
|
||||
<div class="col flex-grow-0">
|
||||
<a href="myorders/@item.Order.OrderId" class="btn btn-success">
|
||||
Verfolgen >
|
||||
</a>
|
||||
</div>
|
||||
</Item>
|
||||
</TemplatedList>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
async Task<IEnumerable<OrderWithStatus>> LoadOrders()
|
||||
{
|
||||
var ordersWithStatus = Enumerable.Empty<OrderWithStatus>();
|
||||
try
|
||||
{
|
||||
ordersWithStatus = await OrdersClient.GetOrders();
|
||||
}
|
||||
catch (AccessTokenNotAvailableException ex)
|
||||
{
|
||||
ex.Redirect();
|
||||
}
|
||||
return ordersWithStatus;
|
||||
}
|
||||
}
|
95
BlazingPizza.Client/Pages/OrderDetails.razor
Normal file
@ -0,0 +1,95 @@
|
||||
@page "/myorders/{orderId:int}"
|
||||
@attribute [Authorize]
|
||||
@using System.Threading
|
||||
@inject OrdersClient OrdersClient
|
||||
@implements IDisposable
|
||||
|
||||
<div class="main">
|
||||
@if (invalidOrder)
|
||||
{
|
||||
<h2>NEIN</h2>
|
||||
<p>Konnte keine Bestellungen laden</p>
|
||||
}
|
||||
else if (orderWithStatus == null)
|
||||
{
|
||||
<text>Lade...</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="track-order">
|
||||
<div class="track-order-title">
|
||||
<h2>
|
||||
Bestellt am @orderWithStatus.Order.CreatedTime.ToLongDateString()
|
||||
</h2>
|
||||
<p class="ml-auto mb-0">
|
||||
Status: <strong>@orderWithStatus.StatusText</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="track-order-body">
|
||||
<div class="track-order-details">
|
||||
<OrderReview Order="orderWithStatus.Order" />
|
||||
</div>
|
||||
<div class="track-order-map">
|
||||
<Map Zoom="13" Markers="orderWithStatus.MapMarkers" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public int OrderId { get; set; }
|
||||
|
||||
OrderWithStatus orderWithStatus;
|
||||
bool invalidOrder;
|
||||
CancellationTokenSource pollingCancellationToken;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// If we were already polling for a different order, stop doing so
|
||||
pollingCancellationToken?.Cancel();
|
||||
|
||||
// Start a new poll loop
|
||||
PollForUpdates();
|
||||
}
|
||||
|
||||
private async void PollForUpdates()
|
||||
{
|
||||
invalidOrder = false;
|
||||
pollingCancellationToken = new CancellationTokenSource();
|
||||
while (!pollingCancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
orderWithStatus = await OrdersClient.GetOrder(OrderId);
|
||||
StateHasChanged();
|
||||
|
||||
if (orderWithStatus.IsDelivered)
|
||||
{
|
||||
pollingCancellationToken.Cancel();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(4000);
|
||||
}
|
||||
}
|
||||
catch (AccessTokenNotAvailableException ex)
|
||||
{
|
||||
pollingCancellationToken.Cancel();
|
||||
ex.Redirect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
invalidOrder = true;
|
||||
pollingCancellationToken.Cancel();
|
||||
Console.Error.WriteLine(ex);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
pollingCancellationToken?.Cancel();
|
||||
}
|
||||
}
|
9
BlazingPizza.Client/PizzaAuthenticationState.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
|
||||
namespace BlazingPizza.Client
|
||||
{
|
||||
public class PizzaAuthenticationState : RemoteAuthenticationState
|
||||
{
|
||||
public Order Order { get; set; }
|
||||
}
|
||||
}
|
31
BlazingPizza.Client/Program.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazingPizza.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) });
|
||||
builder.Services.AddHttpClient<OrdersClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
|
||||
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
|
||||
builder.Services.AddScoped<OrderState>();
|
||||
|
||||
// Add auth services
|
||||
builder.Services.AddApiAuthorization<PizzaAuthenticationState>(options =>
|
||||
{
|
||||
options.AuthenticationPaths.LogOutSucceededPath = "";
|
||||
});
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
}
|
||||
}
|
||||
}
|
51
BlazingPizza.Client/Shared/AddressEditor.razor
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="form-field">
|
||||
<label>Name:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.Name" />
|
||||
<ValidationMessage For="@(() => Address.Name)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Adresse 1:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.Line1" />
|
||||
<ValidationMessage For="@(() => Address.Line1)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Adresse 2:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.Line2" />
|
||||
<ValidationMessage For="@(() => Address.Line2)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Stadt:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.City" />
|
||||
<ValidationMessage For="@(() => Address.City)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>Region:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.Region" />
|
||||
<ValidationMessage For="@(() => Address.Region)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label>PLZ:</label>
|
||||
<div>
|
||||
<InputText @bind-Value="Address.PostalCode" />
|
||||
<ValidationMessage For="@(() => Address.PostalCode)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public Address Address { get; set; }
|
||||
}
|
91
BlazingPizza.Client/Shared/ConfigurePizzaDialog.razor
Normal file
@ -0,0 +1,91 @@
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<div class="dialog-title">
|
||||
<h2>@Pizza.Special.Name</h2>
|
||||
@Pizza.Special.Description
|
||||
</div>
|
||||
<form class="dialog-body">
|
||||
<div>
|
||||
<label>Größe:</label>
|
||||
<input type="range" min="@Pizza.MinimumSize" max="@Pizza.MaximumSize" step="1" @bind="Pizza.Size" @bind:event="oninput" />
|
||||
<span class="size-label">
|
||||
@(Pizza.Size)" (€@(Pizza.GetFormattedTotalPrice()))
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>Extra Beläge:</label>
|
||||
@if (toppings == null)
|
||||
{
|
||||
<select class="custom-select" disabled>
|
||||
<option>(Lade...)</option>
|
||||
</select>
|
||||
}
|
||||
else if (Pizza.Toppings.Count >= 6)
|
||||
{
|
||||
<div>(maximale Anzahl erreicht)</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<select class="custom-select" @onchange="ToppingSelected">
|
||||
<option value="-1" disabled selected>(select)</option>
|
||||
@for (var i = 0; i < toppings.Count; i++)
|
||||
{
|
||||
<option value="@i">@toppings[i].Name - (€@(toppings[i].GetFormattedPrice()))</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="toppings">
|
||||
@foreach (var topping in Pizza.Toppings)
|
||||
{
|
||||
<div class="topping">
|
||||
@topping.Topping.Name
|
||||
<span class="topping-price">@topping.Topping.GetFormattedPrice()</span>
|
||||
<button type="button" class="delete-topping" @onclick="@(() => RemoveTopping(topping.Topping))">x</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn btn-secondary mr-auto" @onclick="OnCancel">Abbrechen</button>
|
||||
<span class="mr-center">
|
||||
Kosten: <span class="price">@(Pizza.GetFormattedTotalPrice())</span>
|
||||
</span>
|
||||
<button class="btn btn-success ml-auto" @onclick="OnConfirm">Bestellen ></button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
List<Topping> toppings;
|
||||
|
||||
[Parameter] public Pizza Pizza { get; set; }
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
[Parameter] public EventCallback OnConfirm { get; set; }
|
||||
|
||||
protected async override Task OnInitializedAsync()
|
||||
{
|
||||
toppings = await HttpClient.GetFromJsonAsync<List<Topping>>("toppings");
|
||||
}
|
||||
|
||||
void ToppingSelected(ChangeEventArgs e)
|
||||
{
|
||||
if (int.TryParse((string)e.Value, out var index) && index >= 0)
|
||||
{
|
||||
AddTopping(toppings[index]);
|
||||
}
|
||||
}
|
||||
|
||||
void AddTopping(Topping topping)
|
||||
{
|
||||
if (Pizza.Toppings.Find(pt => pt.Topping == topping) == null)
|
||||
{
|
||||
Pizza.Toppings.Add(new PizzaTopping() { Topping = topping });
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveTopping(Topping topping)
|
||||
{
|
||||
Pizza.Toppings.RemoveAll(pt => pt.Topping == topping);
|
||||
}
|
||||
}
|
18
BlazingPizza.Client/Shared/ConfiguredPizzaItem.razor
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="cart-item">
|
||||
<a @onclick="OnRemoved" class="delete-item">x</a>
|
||||
<div class="title">@(Pizza.Size)" @Pizza.Special.Name</div>
|
||||
<ul>
|
||||
@foreach (var topping in Pizza.Toppings)
|
||||
{
|
||||
<li>+ @topping.Topping.Name</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="item-price">
|
||||
@Pizza.GetFormattedTotalPrice()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public Pizza Pizza { get; set; }
|
||||
[Parameter] public EventCallback OnRemoved { get; set; }
|
||||
}
|
29
BlazingPizza.Client/Shared/LoginDisplay.razor
Normal file
@ -0,0 +1,29 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject SignOutSessionStateManager SignOutManager
|
||||
|
||||
<div class="user-info">
|
||||
<AuthorizeView>
|
||||
<Authorizing>
|
||||
<text>...</text>
|
||||
</Authorizing>
|
||||
<Authorized>
|
||||
<img src="img/user.svg" />
|
||||
<div>
|
||||
<a href="authentication/profile" class="username">@context.User.Identity.Name</a>
|
||||
<button class="btn btn-link sign-out" @onclick="BeginSignOut">Ausloggen</button>
|
||||
</div>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a class="sign-in" href="authentication/register">Registrieren</a>
|
||||
<a class="sign-in" href="authentication/login">Einloggen</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</div>
|
||||
|
||||
@code{
|
||||
async Task BeginSignOut()
|
||||
{
|
||||
await SignOutManager.SetSignOutState();
|
||||
Navigation.NavigateTo("authentication/logout");
|
||||
}
|
||||
}
|
30
BlazingPizza.Client/Shared/MainLayout.razor
Normal file
@ -0,0 +1,30 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="top-bar">
|
||||
<a class="logo" href="">
|
||||
<img src="img/logo.png" />
|
||||
</a>
|
||||
|
||||
<NavLink href="" class="nav-tab" Match="NavLinkMatch.All">
|
||||
<img src="img/pizza-slice.svg" />
|
||||
<div>Pizza</div>
|
||||
</NavLink>
|
||||
|
||||
<AuthorizeView>
|
||||
<NavLink href="myorders" class="nav-tab">
|
||||
<img src="img/bike.svg" />
|
||||
<div>Meine Bestellungen</div>
|
||||
</NavLink>
|
||||
</AuthorizeView>
|
||||
|
||||
<NavLink href="/impressum" class="nav-tab" Match="NavLinkMatch.All">
|
||||
<img src="img/pizza-slice.svg" />
|
||||
<div>Impressum</div>
|
||||
</NavLink>
|
||||
|
||||
<LoginDisplay />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
@Body
|
||||
</div>
|
28
BlazingPizza.Client/Shared/OrderReview.razor
Normal file
@ -0,0 +1,28 @@
|
||||
@foreach (var pizza in Order.Pizzas)
|
||||
{
|
||||
<p>
|
||||
<strong>
|
||||
@(pizza.Size)"
|
||||
@pizza.Special.Name
|
||||
(€@pizza.GetFormattedTotalPrice())
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
@foreach (var topping in pizza.Toppings)
|
||||
{
|
||||
<li>+ @topping.Topping.Name</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
|
||||
<p>
|
||||
<strong>
|
||||
Gesamt:
|
||||
€@Order.GetFormattedTotalPrice()
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
@code {
|
||||
[Parameter] public Order Order { get; set; }
|
||||
}
|
7
BlazingPizza.Client/Shared/RedirectToLogin.razor
Normal file
@ -0,0 +1,7 @@
|
||||
@inject NavigationManager Navigation
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Navigation.NavigateTo($"authentication/login?returnUrl={Navigation.Uri}");
|
||||
}
|
||||
}
|
15
BlazingPizza.Client/_Imports.razor
Normal file
@ -0,0 +1,15 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Headers
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
@using Microsoft.JSInterop
|
||||
@using BlazingPizza.Client
|
||||
@using BlazingPizza.Client.Shared
|
||||
@using BlazingPizza.ComponentsLibrary
|
||||
@using BlazingPizza.ComponentsLibrary.Map
|
||||
@using BlazingComponents
|
7
BlazingPizza.Client/wwwroot/css/bootstrap/bootstrap.min.css
vendored
Normal file
BIN
BlazingPizza.Client/wwwroot/css/font/quicksand-v8-latin-300.woff
Normal file
BIN
BlazingPizza.Client/wwwroot/css/font/quicksand-v8-latin-500.woff
Normal file
BIN
BlazingPizza.Client/wwwroot/css/font/quicksand-v8-latin-700.woff
Normal file
40
BlazingPizza.Client/wwwroot/css/font/quicksand.css
Normal file
@ -0,0 +1,40 @@
|
||||
/* quicksand-300 - latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Quicksand Light'), local('Quicksand-Light'),
|
||||
url('quicksand-v8-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('quicksand-v8-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* quicksand-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Quicksand Regular'), local('Quicksand-Regular'),
|
||||
url('quicksand-v8-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('quicksand-v8-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* quicksand-500 - latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Quicksand Medium'), local('Quicksand-Medium'),
|
||||
url('quicksand-v8-latin-500.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('quicksand-v8-latin-500.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* quicksand-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Quicksand';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Quicksand Bold'), local('Quicksand-Bold'),
|
||||
url('quicksand-v8-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('quicksand-v8-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
758
BlazingPizza.Client/wwwroot/css/site.css
Normal file
@ -0,0 +1,758 @@
|
||||
@import url('font/quicksand.css');
|
||||
|
||||
body, html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 5rem;
|
||||
flex-direction: column;
|
||||
font-family: 'quicksand';
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-group.row > .col-form-label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 5rem;
|
||||
background-color: rgb(192,0,0);
|
||||
background-image: linear-gradient(rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.25) 25%, rgba(0,0,0,0) 70%);
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
color: white;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
padding: 0 3rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logo > img {
|
||||
margin-right: 3rem;
|
||||
width: 9rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.main {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
background: linear-gradient(rgba(0,0,0,0) 40%, rgba(0,0,0,0.4) 80%);
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
margin: 0;
|
||||
padding: 0.3rem 1.8rem;
|
||||
display: inline-block;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
color: white;
|
||||
position: relative;
|
||||
text-transform: uppercase;
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
.nav-tab:hover {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
color: #fff2cc;
|
||||
}
|
||||
|
||||
.nav-tab img {
|
||||
height: 2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-tab.active img {
|
||||
filter: brightness(0) saturate(100%) invert(93%) sepia(18%) saturate(797%) hue-rotate(316deg) brightness(109%) contrast(101%);
|
||||
}
|
||||
|
||||
.nav-tab.active:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -1rem;
|
||||
z-index: 1;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border-left: 0.6rem solid transparent;
|
||||
border-right: 0.6rem solid transparent;
|
||||
border-top: 1rem solid rgb(205,51,51);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.user-info img {
|
||||
margin-right: 0.7rem;
|
||||
width: 2.6rem;
|
||||
}
|
||||
|
||||
.user-info .username {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
line-height: 0.7rem;
|
||||
margin-top: 0.5rem;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.user-info a {
|
||||
color: #fff2cc;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.user-info button.sign-out {
|
||||
color: #fff2cc;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pizza-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 20rem);
|
||||
grid-gap: 2rem;
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.pizza-cards > li {
|
||||
height: 10rem;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
border-radius: 0.5rem;
|
||||
list-style-type: none;
|
||||
box-shadow: 0 3px 4px rgba(0,0,0,0.4);
|
||||
transition: 0.1s ease-out;
|
||||
}
|
||||
|
||||
.pizza-cards > li:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.pizza-info {
|
||||
border-radius: 0.5rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
background: linear-gradient(rgba(0,0,0,0.7) 30%, rgba(0,0,0,0) 80%);
|
||||
padding: 1rem 1rem;
|
||||
color: #fff2cc;
|
||||
cursor: pointer;
|
||||
text-shadow: 0 2px 2px rgba(0,0,0,0.5);
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.pizza-info .title {
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
display: block;
|
||||
margin: 0.2rem 0 0.4rem 0;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pizza-info .price {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 1rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
padding: 0rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
background-color: #08af08;
|
||||
color: white;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.price::before {
|
||||
content: '€';
|
||||
font-weight: 300;
|
||||
font-size: 1.2rem;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #2b2b2b;
|
||||
width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.order-contents {
|
||||
overflow-y: auto;
|
||||
padding: 2rem 1.5rem 1.5rem 1.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.order-contents h2 {
|
||||
color: #fff2cc;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.order-total {
|
||||
background-color: rgb(191, 52, 52);
|
||||
height: 4rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
transition: all 600ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.order-total.hidden {
|
||||
transform: translate3d(0, 4rem, 0);
|
||||
}
|
||||
|
||||
.order-total .total-price {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.order-total .total-price::before {
|
||||
content: '€';
|
||||
font-weight: 300;
|
||||
margin: 0 0.1rem 0 0.4rem;
|
||||
}
|
||||
|
||||
.order-total .btn {
|
||||
margin-left: auto;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
}
|
||||
|
||||
.checkout-button {
|
||||
margin: auto;
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
border-radius: 20px;
|
||||
padding: 0.4rem 1.2rem;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
background-color: #333333;
|
||||
padding: 0.8rem 1.2rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 100;
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cart-item .title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cart-item ul {
|
||||
padding: 0;
|
||||
margin: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.cart-item li {
|
||||
list-style-type: none;
|
||||
margin-left: 0rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 100;
|
||||
color: #676767;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-price::before {
|
||||
content: '€';
|
||||
font-weight: 100;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
content: 'X';
|
||||
cursor: pointer;
|
||||
color: #fff2cc;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-item:hover {
|
||||
text-decoration: none;
|
||||
color: #fff2cc;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.configured-pizza-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
animation: dialog-container-entry 0.2s;
|
||||
}
|
||||
|
||||
@keyframes dialog-container-entry {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
box-shadow: 0 0 12px rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 2000;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
width: 700px;
|
||||
max-height: calc(100% - 3rem);
|
||||
animation: dialog-entry 0.4s;
|
||||
animation-timing-function: cubic-bezier(0.075, 0.820, 0.165, 1.000);
|
||||
}
|
||||
|
||||
@keyframes dialog-entry {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(0px) scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
background-color: #444;
|
||||
color: #fff2cc;
|
||||
padding: 1.3rem 2rem;
|
||||
}
|
||||
|
||||
.dialog-title h2 {
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
margin: 0;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem 3rem 1rem 0;
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
height: 4rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #eee;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.dialog-body > div {
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dialog-body label {
|
||||
text-align: right;
|
||||
width: 200px;
|
||||
margin: 0 1.5rem;
|
||||
}
|
||||
|
||||
.dialog-body input, .dialog-body select {
|
||||
flex-grow: 1;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.dialog-body .size-label {
|
||||
min-width: 110px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog .toppings {
|
||||
text-align: center;
|
||||
display: block;
|
||||
padding-left: 4rem;
|
||||
}
|
||||
|
||||
.dialog .topping {
|
||||
display: inline-block;
|
||||
background-color: #a04343;
|
||||
color: white;
|
||||
padding: 0.2rem 1rem;
|
||||
border-radius: 2rem;
|
||||
margin: 0.4rem 0.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dialog .topping-price {
|
||||
font-weight: 100;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.dialog .topping-price::before {
|
||||
content: '€';
|
||||
}
|
||||
|
||||
.delete-topping {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.2rem 0.2rem 0.3rem 0.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.delete-topping:hover {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
.form-message {
|
||||
padding: 0.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.dialog .price {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.orders-list .list-group-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.orders-list .col {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.orders-list h5 {
|
||||
color: #c03939;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 300;
|
||||
margin: 0.2rem 0 0 0;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.track-order {
|
||||
background-color: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.4);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.track-order > div {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.track-order-title {
|
||||
background-color: #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem 3rem;
|
||||
}
|
||||
|
||||
.track-order-title h2 {
|
||||
color: #c03939;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 300;
|
||||
margin: 0rem;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.track-order-body {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.track-order-details {
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem 3rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.track-order-map {
|
||||
width: 350px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
a.sign-in {
|
||||
background: none;
|
||||
border: 1.5px solid white;
|
||||
border-radius: 0.7em;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
padding: 0.2rem 0.8rem 0.1rem 0.8rem;
|
||||
font-family: 'Bahnschrift', Arial, Helvetica, sans-serif;
|
||||
font-weight: 100;
|
||||
cursor: pointer;
|
||||
transition: 0.2s ease-out;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
a.sign-in:hover {
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
color: #fff2cc;
|
||||
border-color: #fff2cc;
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
margin: 7.1px 0;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
input[type=range]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 5.8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 1px #000000, 0px 0px 0px #0d0d0d;
|
||||
background: #dcdcdc;
|
||||
border-radius: 1.3px;
|
||||
border: 0px solid #010101;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow: 0.9px 0.9px 1px rgba(0, 0, 49, 0.43), 0px 0px 0.9px rgba(0, 0, 75, 0.43);
|
||||
border: 0px solid #00001e;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background: #d45352;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7.1px;
|
||||
}
|
||||
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background: #e1e1e1;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 5.8px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 0px 1px #000000, 0px 0px 0px #0d0d0d;
|
||||
background: #dcdcdc;
|
||||
border-radius: 1.3px;
|
||||
border: 0px solid #010101;
|
||||
}
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 0.9px 0.9px 1px rgba(0, 0, 49, 0.43), 0px 0px 0.9px rgba(0, 0, 75, 0.43);
|
||||
border: 0px solid #00001e;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background: #d45352;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 5.8px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background: #d7d7d7;
|
||||
border: 0px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
box-shadow: 0px 0px 1px #000000, 0px 0px 0px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background: #dcdcdc;
|
||||
border: 0px solid #010101;
|
||||
border-radius: 2.6px;
|
||||
box-shadow: 0px 0px 1px #000000, 0px 0px 0px #0d0d0d;
|
||||
}
|
||||
|
||||
input[type=range]::-ms-thumb {
|
||||
box-shadow: 0.9px 0.9px 1px rgba(0, 0, 49, 0.43), 0px 0px 0.9px rgba(0, 0, 75, 0.43);
|
||||
border: 0px solid #00001e;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 10px;
|
||||
background: #d45352;
|
||||
cursor: pointer;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.checkout-cols {
|
||||
display: flex;
|
||||
}
|
||||
.checkout-cols h4 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.checkout-cols > div {
|
||||
flex: 1;
|
||||
margin: 1rem;
|
||||
border: 1px solid #ddd;
|
||||
background: rgba(255,255,255,0.3);
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
|
||||
.checkout-cols > div:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.checkout-cols > div:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
top: calc(50% - 3px);
|
||||
left: calc(50% - 250px);
|
||||
width: 500px;
|
||||
height: 6px;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.2)
|
||||
}
|
||||
|
||||
.loading-bar::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: #dc105a;
|
||||
animation: progressbar-slide 1s infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field > label {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.form-field > div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.form-field input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||
display: none;
|
||||
left: 0;
|
||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
@keyframes progressbar-slide {
|
||||
0% {
|
||||
transform: translateX(-200px);
|
||||
}
|
||||
|
||||
70% {
|
||||
transform: translateX(500px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(500px);
|
||||
}
|
||||
}
|
77
BlazingPizza.Client/wwwroot/img/bike.svg
Normal file
@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="14.566216mm"
|
||||
height="12.915195mm"
|
||||
viewBox="0 0 14.566216 12.915195"
|
||||
version="1.1"
|
||||
id="svg6773"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="bike.svg">
|
||||
<defs
|
||||
id="defs6767" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-495.3304"
|
||||
inkscape:cy="-464.16476"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3018"
|
||||
inkscape:window-height="1744"
|
||||
inkscape:window-x="665"
|
||||
inkscape:window-y="3070"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata6770">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-14.639502,-13.107878)">
|
||||
<path
|
||||
d="m 24.422045,14.741241 c 0,0.80786 -0.65263,1.4605 -1.4605,1.4605 -0.80786,0 -1.46402,-0.65264 -1.46402,-1.4605 0,-0.80786 0.65616,-1.46403 1.46402,-1.46403 0.80787,0 1.4605,0.65617 1.4605,1.46403 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.33866665;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4889"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 19.426715,23.546573 c 0,1.273528 -1.03364,2.307167 -2.30717,2.307167 -1.27352,0 -2.30716,-1.033639 -2.30716,-2.307167 0,-1.273528 1.03364,-2.307166 2.30716,-2.307166 1.27353,0 2.30717,1.033638 2.30717,2.307166 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.33866665;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4891"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 29.036385,23.546573 c 0,1.273528 -1.03364,2.307167 -2.30717,2.307167 -1.27353,0 -2.30717,-1.033639 -2.30717,-2.307167 0,-1.273528 1.03364,-2.307166 2.30717,-2.307166 1.27353,0 2.30717,1.033638 2.30717,2.307166 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.33866665;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4893"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 22.587605,19.711881 -1.04422,-0.10231 0.98072,-1.70039 2.23661,1.21709 c -0.0212,0.0212 -0.0212,0.0423 -0.0423,0.0635 l -1.7533,0.98425 v -0.0423 c 0,-0.20814 -0.16934,-0.39864 -0.37748,-0.4198 z m 4.13808,0.88194 c 0.39864,0 0.77611,0.0847 1.10772,0.22931 0.21167,-0.37747 0.35631,-0.81845 0.35631,-1.27706 0,-1.38289 -1.12889,-2.5153 -2.50825,-2.5153 -0.2928,0 -0.56444,0.0635 -0.83608,0.14816 0.10583,0.20814 0.1658,0.43745 0.18697,0.69145 l -2.82222,-1.55222 c -0.0423,-0.0212 -0.0811,-0.0423 -0.12347,-0.0423 -0.48331,-0.18697 -1.02659,-0.0212 -1.29823,0.44097 l -1.46402,2.51531 c -0.0176,0.0423 -0.0388,0.10231 -0.06,0.14464 l -4.45558,-0.45861 v 0.83608 l 3.13619,0.96308 c 1.21356,0.35631 2.09198,1.488727 2.09198,2.808115 0,0.296334 -0.0423,0.567973 -0.127,0.839612 h 4.01461 c -0.0811,-0.271639 -0.12347,-0.543278 -0.12347,-0.839612 0,-1.612194 1.31586,-2.931585 2.92452,-2.931585 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:0.33866665;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4895"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
BIN
BlazingPizza.Client/wwwroot/img/icon-512.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
BlazingPizza.Client/wwwroot/img/logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
117
BlazingPizza.Client/wwwroot/img/logo.svg
Normal file
After Width: | Height: | Size: 43 KiB |
106
BlazingPizza.Client/wwwroot/img/pizza-slice.svg
Normal file
@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="12.864264mm"
|
||||
height="12.890507mm"
|
||||
viewBox="0 0 12.864264 12.890507"
|
||||
version="1.1"
|
||||
id="svg7666"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="pizza-slice.svg">
|
||||
<defs
|
||||
id="defs7660">
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4885">
|
||||
<path
|
||||
d="M -3.9725e-5,540 H 960 V 1.2207e-4 L -4.3448e-5,6.1035e-5"
|
||||
id="path4883"
|
||||
inkscape:connector-curvature="0"
|
||||
style="clip-rule:evenodd" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
clipPathUnits="userSpaceOnUse"
|
||||
id="clipPath4875">
|
||||
<path
|
||||
d="M -1.9395e-5,540 H 960 V 1.2207e-4 L -2.3118e-5,6.1035e-5"
|
||||
id="path4873"
|
||||
inkscape:connector-curvature="0"
|
||||
style="clip-rule:evenodd" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-538.54671"
|
||||
inkscape:cy="-372.78284"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3018"
|
||||
inkscape:window-height="1744"
|
||||
inkscape:window-x="665"
|
||||
inkscape:window-y="3070"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7663">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-26.073816,-37.310701)">
|
||||
<g
|
||||
id="g4869"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-82.464891,223.70045)"
|
||||
style="stroke:#ffffff;stroke-opacity:1">
|
||||
<g
|
||||
id="g4871"
|
||||
clip-path="url(#clipPath4875)"
|
||||
style="stroke:#ffffff;stroke-opacity:1">
|
||||
<path
|
||||
d="m 331.92,516.06 c 0,0 -0.03,0.03 0,0 -0.03,0.03 -0.03,0.03 -0.03,0.03 -3.14,3.13 -6.77,5.57 -10.84,7.23 0,0 0,0 0,0 l -12.47,-30.6 30.61,12.47 c 0,0 0,0 0,0 -1.7,4.1 -4.13,7.73 -7.27,10.87 z m -10.3,-1.7 c -1.13,1.13 -1.13,2.93 -0.03,4.03 1.1,1.1 2.9,1.1 4.03,-0.03 1.13,-1.14 1.13,-2.94 0.03,-4.04 -1.1,-1.1 -2.9,-1.1 -4.03,0.04 z m -8.27,-14.94 2.3,5.63 c 0.5,-0.16 0.97,-0.36 1.37,-0.76 1.2,-1.2 1.2,-3.14 0,-4.34 -1,-1 -2.5,-1.16 -3.67,-0.53 z m 12.14,5.07 c -1.14,1.13 -1.14,2.93 -0.04,4.03 1.1,1.1 2.9,1.1 4.04,-0.03 1.13,-1.14 1.13,-2.94 0.03,-4.04 -1.1,-1.1 -2.9,-1.1 -4.03,0.04 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4877"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
id="g4879"
|
||||
transform="matrix(0.35277777,0,0,-0.35277777,-82.464891,223.70045)"
|
||||
style="stroke:#ffffff;stroke-opacity:1">
|
||||
<g
|
||||
id="g4881"
|
||||
clip-path="url(#clipPath4885)"
|
||||
style="stroke:#ffffff;stroke-opacity:1">
|
||||
<path
|
||||
d="m 343.52,506.99 c -1.93,4.67 -4.73,8.87 -8.3,12.43 -3.6,3.6 -7.77,6.37 -12.37,8.31 -0.73,0.33 -1.56,-0.04 -1.83,-0.77 -0.27,-0.73 0.07,-1.53 0.8,-1.87 4.23,-1.76 8.07,-4.33 11.4,-7.67 3.34,-3.33 5.9,-7.16 7.7,-11.5 0.07,-0.2 0.2,-0.33 0.34,-0.47 0.4,-0.4 1,-0.53 1.53,-0.33 0.7,0.3 1.03,1.17 0.73,1.87 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4887"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.1 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/bacon.jpg
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/cheese.jpg
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/margherita.jpg
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/meaty.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/mushroom.jpg
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/pepperoni.jpg
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
BlazingPizza.Client/wwwroot/img/pizzas/salad.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
72
BlazingPizza.Client/wwwroot/img/user.svg
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="15.113004mm"
|
||||
height="15.113004mm"
|
||||
viewBox="0 0 15.113004 15.113004"
|
||||
version="1.1"
|
||||
id="svg7709"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"
|
||||
sodipodi:docname="user.svg">
|
||||
<defs
|
||||
id="defs7703" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="0.35"
|
||||
inkscape:cx="-578.58286"
|
||||
inkscape:cy="28.560012"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3018"
|
||||
inkscape:window-height="1744"
|
||||
inkscape:window-x="665"
|
||||
inkscape:window-y="3070"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata7706">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-98.276833,-141.27683)">
|
||||
<path
|
||||
d="m 98.806,148.83333 c 0,-3.88055 3.14678,-7.02733 7.02733,-7.02733 3.88056,0 7.02734,3.14678 7.02734,7.02733 0,3.88056 -3.14678,7.02734 -7.02734,7.02734 -3.88055,0 -7.02733,-3.14678 -7.02733,-7.02734 z"
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1.05833328;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path4913"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 108.23222,147.28111 c 0,1.2947 -1.04775,2.34245 -2.34244,2.34245 -1.29117,0 -2.33892,-1.04775 -2.33892,-2.34245 0,-1.29117 1.04775,-2.33892 2.33892,-2.33892 1.29469,0 2.34244,1.04775 2.34244,2.33892 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.35277775"
|
||||
id="path4915"
|
||||
inkscape:connector-curvature="0" />
|
||||
<path
|
||||
d="m 110.51117,153.92392 0.0847,-1.66864 c 0,-0.35278 -0.17639,-0.70203 -0.46919,-0.93839 -0.64206,-0.52564 -1.4605,-0.87842 -2.27895,-1.11125 -0.58561,-0.17639 -1.22766,-0.29281 -1.92969,-0.29281 -0.64206,0 -1.28764,0.11642 -1.92969,0.29281 -0.81845,0.23283 -1.63689,0.64558 -2.27895,1.11125 -0.2928,0.23636 -0.46919,0.58561 -0.46919,0.93839 v 1.73214 c 1.46755,0.40569 0.98777,1.64747 4.70605,1.54869 4.36034,-0.25753 2.94217,-1.18533 4.56495,-1.61219 z"
|
||||
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.35277775"
|
||||
id="path4917"
|
||||
inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
34
BlazingPizza.Client/wwwroot/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<base href="/" />
|
||||
<link rel="icon" href="img/icon-512.png" />
|
||||
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="css/site.css" rel="stylesheet" />
|
||||
<link href="_content/BlazingPizza.ComponentsLibrary/leaflet/leaflet.css" rel="stylesheet" />
|
||||
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<title>Lodernd Pizza</title>
|
||||
</head>
|
||||
<body>
|
||||
<app>
|
||||
<div class="loading-bar"></div>
|
||||
</app>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
Ein fehler ist aufgetreten
|
||||
<a href="" class="reload">Neu laden</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/BlazingPizza.ComponentsLibrary/localStorage.js"></script>
|
||||
<script src="_content/BlazingPizza.ComponentsLibrary/pushNotifications.js"></script>
|
||||
<script src="_content/BlazingPizza.ComponentsLibrary/deliveryMap.js"></script>
|
||||
<script src="_content/BlazingPizza.ComponentsLibrary/leaflet/leaflet.js"></script>
|
||||
<script>navigator.serviceWorker.register('service-worker.js');</script>
|
||||
</body>
|
||||
</html>
|
16
BlazingPizza.Client/wwwroot/manifest.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"short_name": "Blazing Pizza",
|
||||
"name": "Blazing Pizza",
|
||||
"icons": [
|
||||
{
|
||||
"src": "img/icon-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
"background_color": "#860000",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#860000"
|
||||
}
|
27
BlazingPizza.Client/wwwroot/service-worker.js
Normal file
@ -0,0 +1,27 @@
|
||||
self.addEventListener('install', async event => {
|
||||
console.log('Installing service worker...');
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
// You can add custom logic here for controlling whether to use cached data if offline, etc.
|
||||
// The following line opts out, so requests go directly to the network as usual.
|
||||
return null;
|
||||
});
|
||||
|
||||
self.addEventListener('push', event => {
|
||||
const payload = event.data.json();
|
||||
event.waitUntil(
|
||||
self.registration.showNotification('Blazing Pizza', {
|
||||
body: payload.message,
|
||||
icon: 'img/icon-512.png',
|
||||
vibrate: [100, 50, 100],
|
||||
data: { url: payload.url }
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', event => {
|
||||
event.notification.close();
|
||||
event.waitUntil(clients.openWindow(event.notification.data.url));
|
||||
});
|
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<RazorLangVersion>3.0</RazorLangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(AspNetCoreVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
17
BlazingPizza.ComponentsLibrary/LocalStorage.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Microsoft.JSInterop;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazingPizza.ComponentsLibrary
|
||||
{
|
||||
public static class LocalStorage
|
||||
{
|
||||
public static ValueTask<T> GetAsync<T>(IJSRuntime jsRuntime, string key)
|
||||
=> jsRuntime.InvokeAsync<T>("blazorLocalStorage.get", key);
|
||||
|
||||
public static ValueTask SetAsync(IJSRuntime jsRuntime, string key, object value)
|
||||
=> jsRuntime.InvokeVoidAsync("blazorLocalStorage.set", key, value);
|
||||
|
||||
public static ValueTask DeleteAsync(IJSRuntime jsRuntime, string key)
|
||||
=> jsRuntime.InvokeVoidAsync("blazorLocalStorage.delete", key);
|
||||
}
|
||||
}
|
19
BlazingPizza.ComponentsLibrary/Map/Map.razor
Normal file
@ -0,0 +1,19 @@
|
||||
@using Microsoft.JSInterop
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<div id="@elementId" style="height: 100%; width: 100%;"></div>
|
||||
|
||||
@code {
|
||||
string elementId = $"map-{Guid.NewGuid().ToString("D")}";
|
||||
|
||||
[Parameter] public double Zoom { get; set; }
|
||||
[Parameter] public List<Marker> Markers { get; set; }
|
||||
|
||||
protected async override Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"deliveryMap.showOrUpdate",
|
||||
elementId,
|
||||
Markers);
|
||||
}
|
||||
}
|
13
BlazingPizza.ComponentsLibrary/Map/Marker.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace BlazingPizza.ComponentsLibrary.Map
|
||||
{
|
||||
public class Marker
|
||||
{
|
||||
public string Description { get; set; }
|
||||
|
||||
public double X { get; set; }
|
||||
|
||||
public double Y { get; set; }
|
||||
|
||||
public bool ShowPopup { get; set; }
|
||||
}
|
||||
}
|
9
BlazingPizza.ComponentsLibrary/Map/Point.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BlazingPizza.ComponentsLibrary.Map
|
||||
{
|
||||
public class Point
|
||||
{
|
||||
public double X { get; set; }
|
||||
|
||||
public double Y { get; set; }
|
||||
}
|
||||
}
|
79
BlazingPizza.ComponentsLibrary/wwwroot/deliveryMap.js
Normal file
@ -0,0 +1,79 @@
|
||||
(function () {
|
||||
var tileUrl = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
var tileAttribution = 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors, <a href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>';
|
||||
|
||||
// Global export
|
||||
window.deliveryMap = {
|
||||
showOrUpdate: function (elementId, markers) {
|
||||
var elem = document.getElementById(elementId);
|
||||
if (!elem) {
|
||||
throw new Error('No element with ID ' + elementId);
|
||||
}
|
||||
|
||||
// Initialize map if needed
|
||||
if (!elem.map) {
|
||||
elem.map = L.map(elementId);
|
||||
elem.map.addedMarkers = [];
|
||||
L.tileLayer(tileUrl, { attribution: tileAttribution }).addTo(elem.map);
|
||||
}
|
||||
|
||||
var map = elem.map;
|
||||
if (map.addedMarkers.length !== markers.length) {
|
||||
// Markers have changed, so reset
|
||||
map.addedMarkers.forEach(marker => marker.removeFrom(map));
|
||||
map.addedMarkers = markers.map(m => {
|
||||
return L.marker([m.y, m.x]).bindPopup(m.description).addTo(map);
|
||||
});
|
||||
|
||||
// Auto-fit the view
|
||||
var markersGroup = new L.featureGroup(map.addedMarkers);
|
||||
map.fitBounds(markersGroup.getBounds().pad(0.3));
|
||||
|
||||
// Show applicable popups. Can't do this until after the view was auto-fitted.
|
||||
markers.forEach((marker, index) => {
|
||||
if (marker.showPopup) {
|
||||
map.addedMarkers[index].openPopup();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Same number of markers, so update positions/text without changing view bounds
|
||||
markers.forEach((marker, index) => {
|
||||
animateMarkerMove(
|
||||
map.addedMarkers[index].setPopupContent(marker.description),
|
||||
marker,
|
||||
4000);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function animateMarkerMove(marker, coords, durationMs) {
|
||||
if (marker.existingAnimation) {
|
||||
cancelAnimationFrame(marker.existingAnimation.callbackHandle);
|
||||
}
|
||||
|
||||
marker.existingAnimation = {
|
||||
startTime: new Date(),
|
||||
durationMs: durationMs,
|
||||
startCoords: { x: marker.getLatLng().lng, y: marker.getLatLng().lat },
|
||||
endCoords: coords,
|
||||
callbackHandle: window.requestAnimationFrame(() => animateMarkerMoveFrame(marker))
|
||||
};
|
||||
}
|
||||
|
||||
function animateMarkerMoveFrame(marker) {
|
||||
var anim = marker.existingAnimation;
|
||||
var proportionCompleted = (new Date().valueOf() - anim.startTime.valueOf()) / anim.durationMs;
|
||||
var coordsNow = {
|
||||
x: anim.startCoords.x + (anim.endCoords.x - anim.startCoords.x) * proportionCompleted,
|
||||
y: anim.startCoords.y + (anim.endCoords.y - anim.startCoords.y) * proportionCompleted
|
||||
};
|
||||
|
||||
marker.setLatLng([coordsNow.y, coordsNow.x]);
|
||||
|
||||
if (proportionCompleted < 1) {
|
||||
marker.existingAnimation.callbackHandle = window.requestAnimationFrame(
|
||||
() => animateMarkerMoveFrame(marker));
|
||||
}
|
||||
}
|
||||
})();
|
After Width: | Height: | Size: 1.2 KiB |
BIN
BlazingPizza.ComponentsLibrary/wwwroot/leaflet/images/layers.png
Normal file
After Width: | Height: | Size: 696 B |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 618 B |
635
BlazingPizza.ComponentsLibrary/wwwroot/leaflet/leaflet.css
Normal file
@ -0,0 +1,635 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-pane > svg,
|
||||
.leaflet-pane > canvas,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
}
|
||||
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||
.leaflet-safari .leaflet-tile-container {
|
||||
width: 1600px;
|
||||
height: 1600px;
|
||||
-webkit-transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container .leaflet-overlay-pane svg,
|
||||
.leaflet-container .leaflet-marker-pane img,
|
||||
.leaflet-container .leaflet-shadow-pane img,
|
||||
.leaflet-container .leaflet-tile-pane img,
|
||||
.leaflet-container img.leaflet-image-layer,
|
||||
.leaflet-container .leaflet-tile {
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.leaflet-container.leaflet-touch-zoom {
|
||||
-ms-touch-action: pan-x pan-y;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag {
|
||||
-ms-touch-action: pinch-zoom;
|
||||
/* Fallback for FF which doesn't support pinch-zoom */
|
||||
touch-action: none;
|
||||
touch-action: pinch-zoom;
|
||||
}
|
||||
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
.leaflet-container {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.leaflet-container a {
|
||||
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
z-index: 800;
|
||||
}
|
||||
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||
.leaflet-overlay-pane svg {
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.leaflet-pane { z-index: 400; }
|
||||
|
||||
.leaflet-tile-pane { z-index: 200; }
|
||||
.leaflet-overlay-pane { z-index: 400; }
|
||||
.leaflet-shadow-pane { z-index: 500; }
|
||||
.leaflet-marker-pane { z-index: 600; }
|
||||
.leaflet-tooltip-pane { z-index: 650; }
|
||||
.leaflet-popup-pane { z-index: 700; }
|
||||
|
||||
.leaflet-map-pane canvas { z-index: 100; }
|
||||
.leaflet-map-pane svg { z-index: 200; }
|
||||
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 800;
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile {
|
||||
will-change: opacity;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
.leaflet-zoom-animated {
|
||||
-webkit-transform-origin: 0 0;
|
||||
-ms-transform-origin: 0 0;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: transform;
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-grab {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
.leaflet-crosshair,
|
||||
.leaflet-crosshair .leaflet-interactive {
|
||||
cursor: crosshair;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging .leaflet-grab,
|
||||
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||
.leaflet-dragging .leaflet-marker-draggable {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* marker & overlays interactivity */
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-pane > svg path,
|
||||
.leaflet-tile-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #38f;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in,
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
text-indent: 1px;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||
background: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-scrollbar {
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
/* Default icon URLs */
|
||||
.leaflet-default-icon-path {
|
||||
background-image: url(images/marker-icon.png);
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-control-attribution a {
|
||||
text-decoration: none;
|
||||
}
|
||||
.leaflet-control-attribution a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
background: #fff;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-bar {
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -20px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
background: white;
|
||||
color: #333;
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
border: none;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||
zoom: 1;
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
width: 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
}
|
||||
.leaflet-oldie .leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.leaflet-oldie .leaflet-control-zoom,
|
||||
.leaflet-oldie .leaflet-control-layers,
|
||||
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||
.leaflet-oldie .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
|
||||
|
||||
/* Tooltip */
|
||||
/* Base styles for the element that has a tooltip */
|
||||
.leaflet-tooltip {
|
||||
position: absolute;
|
||||
padding: 6px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: #222;
|
||||
white-space: nowrap;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-tooltip.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-tooltip-top:before,
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border: 6px solid transparent;
|
||||
background: transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Directions */
|
||||
|
||||
.leaflet-tooltip-bottom {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.leaflet-tooltip-top {
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before,
|
||||
.leaflet-tooltip-top:before {
|
||||
left: 50%;
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-top:before {
|
||||
bottom: 0;
|
||||
margin-bottom: -12px;
|
||||
border-top-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-bottom:before {
|
||||
top: 0;
|
||||
margin-top: -12px;
|
||||
margin-left: -6px;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-left {
|
||||
margin-left: -6px;
|
||||
}
|
||||
.leaflet-tooltip-right {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before,
|
||||
.leaflet-tooltip-right:before {
|
||||
top: 50%;
|
||||
margin-top: -6px;
|
||||
}
|
||||
.leaflet-tooltip-left:before {
|
||||
right: 0;
|
||||
margin-right: -12px;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
.leaflet-tooltip-right:before {
|
||||
left: 0;
|
||||
margin-left: -12px;
|
||||
border-right-color: #fff;
|
||||
}
|
7
BlazingPizza.ComponentsLibrary/wwwroot/localStorage.js
Normal file
@ -0,0 +1,7 @@
|
||||
(function () {
|
||||
window.blazorLocalStorage = {
|
||||
get: key => key in localStorage ? JSON.parse(localStorage[key]) : null,
|
||||
set: (key, value) => { localStorage[key] = JSON.stringify(value); },
|
||||
delete: key => { delete localStorage[key]; }
|
||||
};
|
||||
})();
|
46
BlazingPizza.ComponentsLibrary/wwwroot/pushNotifications.js
Normal file
@ -0,0 +1,46 @@
|
||||
(function () {
|
||||
// Note: Replace with your own key pair before deploying
|
||||
const applicationServerPublicKey = 'BLC8GOevpcpjQiLkO7JmVClQjycvTCYWm6Cq_a7wJZlstGTVZvwGFFHMYfXt6Njyvgx_GlXJeo5cSiZ1y4JOx1o';
|
||||
|
||||
window.blazorPushNotifications = {
|
||||
requestSubscription: async () => {
|
||||
const worker = await navigator.serviceWorker.getRegistration();
|
||||
const existingSubscription = await worker.pushManager.getSubscription();
|
||||
if (!existingSubscription) {
|
||||
const newSubscription = await subscribe(worker);
|
||||
if (newSubscription) {
|
||||
return {
|
||||
url: newSubscription.endpoint,
|
||||
p256dh: arrayBufferToBase64(newSubscription.getKey('p256dh')),
|
||||
auth: arrayBufferToBase64(newSubscription.getKey('auth'))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function subscribe(worker) {
|
||||
try {
|
||||
return await worker.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerPublicKey
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name === 'NotAllowedError') {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
// https://stackoverflow.com/a/9458996
|
||||
var binary = '';
|
||||
var bytes = new Uint8Array(buffer);
|
||||
var len = bytes.byteLength;
|
||||
for (var i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
})();
|
@ -0,0 +1,31 @@
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using BlazingPizza.Server
|
||||
@inject SignInManager<PizzaStoreUser> SignInManager
|
||||
@inject UserManager<PizzaStoreUser> UserManager
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@{
|
||||
var returnUrl = "/";
|
||||
if (Context.Request.Query.TryGetValue("returnUrl", out var existingUrl))
|
||||
{
|
||||
returnUrl = existingUrl;
|
||||
}
|
||||
}
|
||||
|
||||
<div class="user-info">
|
||||
@if (SignInManager.IsSignedIn(User))
|
||||
{
|
||||
<img src="~/img/user.svg" />
|
||||
<div>
|
||||
<a class="username" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">@User.Identity.Name</a>
|
||||
<form asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="/" method="post">
|
||||
<button type="submit" class="btn btn-link sign-out">Ausloggen</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="sign-in" asp-area="Identity" asp-page="/Account/Register" asp-route-returnUrl="@returnUrl">Registrieren</a>
|
||||
<a class="sign-in" asp-area="Identity" asp-page="/Account/Login" asp-route-returnUrl="@returnUrl">Einloggen</a>
|
||||
}
|
||||
</div>
|
38
BlazingPizza.Server/Areas/Identity/Pages/_Layout.cshtml
Normal file
@ -0,0 +1,38 @@
|
||||
@using Microsoft.AspNetCore.Hosting
|
||||
@using Microsoft.AspNetCore.Mvc.ViewEngines
|
||||
@using BlazingPizza.Client.Shared
|
||||
@inject IWebHostEnvironment Environment
|
||||
@inject ICompositeViewEngine Engine
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="~/img/icon-512.png" rel="icon" />
|
||||
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="~/css/site.css" rel="stylesheet" asp-append-version="true" />
|
||||
<title>Lodern Pizza - @ViewData["Title"]</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-bar">
|
||||
<a class="logo" href="~/">
|
||||
<img src="~/img/logo.png" />
|
||||
</a>
|
||||
<partial name="_LoginPartial" />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="main">
|
||||
@RenderBody()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/Identity/lib/jquery/dist/jquery.min.js"></script>
|
||||
<script src="~/Identity/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="~/Identity/js/site.js" asp-append-version="true"></script>
|
||||
|
||||
@RenderSection("Scripts", required: false)
|
||||
</body>
|
||||
</html>
|
25
BlazingPizza.Server/BlazingPizza.Server.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(BlazorVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="$(AspNetCoreVersion)" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkVersion)" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="$(EntityFrameworkVersion)" />
|
||||
<PackageReference Include="WebPush" Version="1.0.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BlazingPizza.Client\BlazingPizza.Client.csproj" />
|
||||
<ProjectReference Include="..\BlazingPizza.Shared\BlazingPizza.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
43
BlazingPizza.Server/NotificationsController.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
[Route("notifications")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class NotificationsController : Controller
|
||||
{
|
||||
private readonly PizzaStoreContext _db;
|
||||
|
||||
public NotificationsController(PizzaStoreContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpPut("subscribe")]
|
||||
public async Task<NotificationSubscription> Subscribe(NotificationSubscription subscription)
|
||||
{
|
||||
// We're storing at most one subscription per user, so delete old ones.
|
||||
// Alternatively, you could let the user register multiple subscriptions from different browsers/devices.
|
||||
var userId = GetUserId();
|
||||
var oldSubscriptions = _db.NotificationSubscriptions.Where(e => e.UserId == userId);
|
||||
_db.NotificationSubscriptions.RemoveRange(oldSubscriptions);
|
||||
|
||||
// Store new subscription
|
||||
subscription.UserId = userId;
|
||||
_db.NotificationSubscriptions.Attach(subscription);
|
||||
|
||||
await _db.SaveChangesAsync();
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
26
BlazingPizza.Server/OidcConfigurationController.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public class OidcConfigurationController : Controller
|
||||
{
|
||||
public OidcConfigurationController(IClientRequestParametersProvider clientRequestParametersProvider)
|
||||
{
|
||||
ClientRequestParametersProvider = clientRequestParametersProvider;
|
||||
}
|
||||
|
||||
public IClientRequestParametersProvider ClientRequestParametersProvider { get; }
|
||||
|
||||
[HttpGet("_configuration/{clientId}")]
|
||||
public IActionResult GetClientRequestParameters([FromRoute]string clientId)
|
||||
{
|
||||
var parameters = ClientRequestParametersProvider.GetClientParameters(HttpContext, clientId);
|
||||
return Ok(parameters);
|
||||
}
|
||||
}
|
||||
}
|
135
BlazingPizza.Server/OrdersController.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WebPush;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
[Route("orders")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class OrdersController : Controller
|
||||
{
|
||||
private readonly PizzaStoreContext _db;
|
||||
|
||||
public OrdersController(PizzaStoreContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<OrderWithStatus>>> GetOrders()
|
||||
{
|
||||
var orders = await _db.Orders
|
||||
.Where(o => o.UserId == GetUserId())
|
||||
.Include(o => o.DeliveryLocation)
|
||||
.Include(o => o.Pizzas).ThenInclude(p => p.Special)
|
||||
.Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping)
|
||||
.OrderByDescending(o => o.CreatedTime)
|
||||
.ToListAsync();
|
||||
|
||||
return orders.Select(o => OrderWithStatus.FromOrder(o)).ToList();
|
||||
}
|
||||
|
||||
[HttpGet("{orderId}")]
|
||||
public async Task<ActionResult<OrderWithStatus>> GetOrderWithStatus(int orderId)
|
||||
{
|
||||
var order = await _db.Orders
|
||||
.Where(o => o.OrderId == orderId)
|
||||
.Where(o => o.UserId == GetUserId())
|
||||
.Include(o => o.DeliveryLocation)
|
||||
.Include(o => o.Pizzas).ThenInclude(p => p.Special)
|
||||
.Include(o => o.Pizzas).ThenInclude(p => p.Toppings).ThenInclude(t => t.Topping)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (order == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return OrderWithStatus.FromOrder(order);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<int>> PlaceOrder(Order order)
|
||||
{
|
||||
order.CreatedTime = DateTime.Now;
|
||||
order.DeliveryLocation = new LatLong(49.1218574, 9.2117063);
|
||||
order.UserId = GetUserId();
|
||||
|
||||
// Enforce existence of Pizza.SpecialId and Topping.ToppingId
|
||||
// in the database - prevent the submitter from making up
|
||||
// new specials and toppings
|
||||
foreach (var pizza in order.Pizzas)
|
||||
{
|
||||
pizza.SpecialId = pizza.Special.Id;
|
||||
pizza.Special = null;
|
||||
|
||||
foreach (var topping in pizza.Toppings)
|
||||
{
|
||||
topping.ToppingId = topping.Topping.Id;
|
||||
topping.Topping = null;
|
||||
}
|
||||
}
|
||||
|
||||
_db.Orders.Attach(order);
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
// In the background, send push notifications if possible
|
||||
var subscription = await _db.NotificationSubscriptions.Where(e => e.UserId == GetUserId()).SingleOrDefaultAsync();
|
||||
if (subscription != null)
|
||||
{
|
||||
_ = TrackAndSendNotificationsAsync(order, subscription);
|
||||
}
|
||||
|
||||
return order.OrderId;
|
||||
}
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
|
||||
private static async Task TrackAndSendNotificationsAsync(Order order, NotificationSubscription subscription)
|
||||
{
|
||||
// In a realistic case, some other backend process would track
|
||||
// order delivery progress and send us notifications when it
|
||||
// changes. Since we don't have any such process here, fake it.
|
||||
await Task.Delay(OrderWithStatus.PreparationDuration);
|
||||
await SendNotificationAsync(order, subscription, "Your order has been dispatched!");
|
||||
|
||||
await Task.Delay(OrderWithStatus.DeliveryDuration);
|
||||
await SendNotificationAsync(order, subscription, "Your order is now delivered. Enjoy!");
|
||||
}
|
||||
|
||||
private static async Task SendNotificationAsync(Order order, NotificationSubscription subscription, string message)
|
||||
{
|
||||
// For a real application, generate your own
|
||||
var publicKey = "BLC8GOevpcpjQiLkO7JmVClQjycvTCYWm6Cq_a7wJZlstGTVZvwGFFHMYfXt6Njyvgx_GlXJeo5cSiZ1y4JOx1o";
|
||||
var privateKey = "OrubzSz3yWACscZXjFQrrtDwCKg-TGFuWhluQ2wLXDo";
|
||||
|
||||
var pushSubscription = new PushSubscription(subscription.Url, subscription.P256dh, subscription.Auth);
|
||||
var vapidDetails = new VapidDetails("mailto:<someone@example.com>", publicKey, privateKey);
|
||||
var webPushClient = new WebPushClient();
|
||||
try
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(new
|
||||
{
|
||||
message,
|
||||
url = $"myorders/{order.OrderId}",
|
||||
});
|
||||
await webPushClient.SendNotificationAsync(pushSubscription, payload, vapidDetails);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine("Error sending push notification: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
BlazingPizza.Server/PizzaStoreContext.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using IdentityServer4.EntityFramework.Options;
|
||||
using Microsoft.AspNetCore.ApiAuthorization.IdentityServer;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public class PizzaStoreContext : ApiAuthorizationDbContext<PizzaStoreUser>
|
||||
{
|
||||
public PizzaStoreContext(
|
||||
DbContextOptions options,
|
||||
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public DbSet<Order> Orders { get; set; }
|
||||
|
||||
public DbSet<Pizza> Pizzas { get; set; }
|
||||
|
||||
public DbSet<PizzaSpecial> Specials { get; set; }
|
||||
|
||||
public DbSet<Topping> Toppings { get; set; }
|
||||
|
||||
public DbSet<NotificationSubscription> NotificationSubscriptions { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Configuring a many-to-many special -> topping relationship that is friendly for serialization
|
||||
modelBuilder.Entity<PizzaTopping>().HasKey(pst => new { pst.PizzaId, pst.ToppingId });
|
||||
modelBuilder.Entity<PizzaTopping>().HasOne<Pizza>().WithMany(ps => ps.Toppings);
|
||||
modelBuilder.Entity<PizzaTopping>().HasOne(pst => pst.Topping).WithMany();
|
||||
|
||||
// Inline the Lat-Long pairs in Order rather than having a FK to another table
|
||||
modelBuilder.Entity<Order>().OwnsOne(o => o.DeliveryLocation);
|
||||
}
|
||||
}
|
||||
}
|
8
BlazingPizza.Server/PizzaStoreUser.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public class PizzaStoreUser : IdentityUser
|
||||
{
|
||||
}
|
||||
}
|
16
BlazingPizza.Server/PizzasController.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
[Route("pizzas")]
|
||||
[ApiController]
|
||||
public class PizzasController : Controller
|
||||
{
|
||||
private readonly PizzaStoreContext _db;
|
||||
|
||||
public PizzasController(PizzaStoreContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
}
|
||||
}
|
33
BlazingPizza.Server/Program.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var host = CreateHostBuilder(args).Build();
|
||||
|
||||
// Initialize the database
|
||||
var scopeFactory = host.Services.GetRequiredService<IServiceScopeFactory>();
|
||||
using (var scope = scopeFactory.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<PizzaStoreContext>();
|
||||
if (db.Database.EnsureCreated())
|
||||
{
|
||||
SeedData.Initialize(db);
|
||||
}
|
||||
}
|
||||
|
||||
host.Run();
|
||||
}
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||
Host.CreateDefaultBuilder(args)
|
||||
.ConfigureWebHostDefaults(webBuilder =>
|
||||
{
|
||||
webBuilder.UseStartup<Startup>();
|
||||
});
|
||||
}
|
||||
}
|
29
BlazingPizza.Server/Properties/launchSettings.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:64588/",
|
||||
"sslPort": 44381
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"BlazingPizza.Server": {
|
||||
"commandName": "Project",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
70
BlazingPizza.Server/SeedData.cs
Normal file
@ -0,0 +1,70 @@
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public static class SeedData
|
||||
{
|
||||
public static void Initialize(PizzaStoreContext db)
|
||||
{
|
||||
var toppings = new Topping[]
|
||||
{
|
||||
new Topping()
|
||||
{
|
||||
Name = "Ananas",
|
||||
Price = 2.50m,
|
||||
},
|
||||
new Topping()
|
||||
{
|
||||
Name = "Artischocken",
|
||||
Price = 1.50m,
|
||||
},
|
||||
new Topping()
|
||||
{
|
||||
Name = "Pilze",
|
||||
Price = 1.00m,
|
||||
}
|
||||
};
|
||||
|
||||
var specials = new PizzaSpecial[]
|
||||
{
|
||||
new PizzaSpecial()
|
||||
{
|
||||
Name = "Pizza Margherita",
|
||||
Description = "mit Edamer",
|
||||
BasePrice = 9.99m,
|
||||
ImageUrl = "img/pizzas/cheese.jpg",
|
||||
},
|
||||
new PizzaSpecial()
|
||||
{
|
||||
Name = "Pizza Salami",
|
||||
Description = "mit Salami",
|
||||
BasePrice = 8.00m,
|
||||
ImageUrl = "img/pizzas/meaty.jpg",
|
||||
},
|
||||
new PizzaSpecial()
|
||||
{
|
||||
Name = "Pizza Prosciutto",
|
||||
Description = "mit Schinken",
|
||||
BasePrice = 8.00m,
|
||||
ImageUrl = "img/pizzas/bacon.jpg",
|
||||
},
|
||||
new PizzaSpecial()
|
||||
{
|
||||
Name = "Pizza Funghi",
|
||||
Description = "mit Pilzen",
|
||||
BasePrice = 8.00m,
|
||||
ImageUrl = "img/pizzas/mushroom.jpg",
|
||||
},
|
||||
new PizzaSpecial()
|
||||
{
|
||||
Name = "Pizza Gyros",
|
||||
Description = "mit Gyros und frischem Blattspinat",
|
||||
BasePrice = 8.50m,
|
||||
ImageUrl = "img/pizzas/salad.jpg",
|
||||
}
|
||||
};
|
||||
|
||||
db.Toppings.AddRange(toppings);
|
||||
db.Specials.AddRange(specials);
|
||||
db.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
26
BlazingPizza.Server/SpecialsController.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
[Route("specials")]
|
||||
[ApiController]
|
||||
public class SpecialsController : Controller
|
||||
{
|
||||
private readonly PizzaStoreContext _db;
|
||||
|
||||
public SpecialsController(PizzaStoreContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<PizzaSpecial>>> GetSpecials()
|
||||
{
|
||||
return (await _db.Specials.ToListAsync()).OrderByDescending(s => s.BasePrice).ToList();
|
||||
}
|
||||
}
|
||||
}
|
70
BlazingPizza.Server/Startup.cs
Normal file
@ -0,0 +1,70 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc()
|
||||
.AddNewtonsoftJson();
|
||||
|
||||
services.AddDbContext<PizzaStoreContext>(options =>
|
||||
options.UseSqlite("Data Source=pizza.db"));
|
||||
|
||||
services.AddDefaultIdentity<PizzaStoreUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||
.AddEntityFrameworkStores<PizzaStoreContext>();
|
||||
|
||||
services.AddIdentityServer()
|
||||
.AddApiAuthorization<PizzaStoreUser, PizzaStoreContext>();
|
||||
|
||||
services.AddAuthentication()
|
||||
.AddIdentityServerJwt();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseWebAssemblyDebugging();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// 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.UseAuthentication();
|
||||
app.UseIdentityServer();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapFallbackToFile("index.html");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
26
BlazingPizza.Server/ToppingsController.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BlazingPizza.Server
|
||||
{
|
||||
[Route("toppings")]
|
||||
[ApiController]
|
||||
public class ToppingsController : Controller
|
||||
{
|
||||
private readonly PizzaStoreContext _db;
|
||||
|
||||
public ToppingsController(PizzaStoreContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<List<Topping>>> GetToppings()
|
||||
{
|
||||
return await _db.Toppings.OrderBy(t => t.Name).ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
14
BlazingPizza.Server/appsettings.Development.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"IdentityServer": {
|
||||
"Key": {
|
||||
"Type": "Development"
|
||||
}
|
||||
}
|
||||
}
|
19
BlazingPizza.Server/appsettings.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
}
|
||||
},
|
||||
"IdentityServer": {
|
||||
"Clients": {
|
||||
"BlazingPizza.Client": {
|
||||
"Profile": "IdentityServerSPA"
|
||||
}
|
||||
},
|
||||
"Key": {
|
||||
"Type":"Development"
|
||||
}
|
||||
}
|
||||
}
|
27
BlazingPizza.Shared/Address.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class Address
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Required, MaxLength(100)]
|
||||
public string Line1 { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
public string Line2 { get; set; }
|
||||
|
||||
[Required, MaxLength(50)]
|
||||
public string City { get; set; }
|
||||
|
||||
[Required, MaxLength(20)]
|
||||
public string Region { get; set; }
|
||||
|
||||
[Required, MaxLength(20)]
|
||||
public string PostalCode { get; set; }
|
||||
}
|
||||
}
|
13
BlazingPizza.Shared/BlazingPizza.Shared.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
<RootNamespace>BlazingPizza</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BlazingPizza.ComponentsLibrary\BlazingPizza.ComponentsLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
27
BlazingPizza.Shared/LatLong.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class LatLong
|
||||
{
|
||||
public LatLong()
|
||||
{
|
||||
}
|
||||
|
||||
public LatLong(double latitude, double longitude) : this()
|
||||
{
|
||||
Latitude = latitude;
|
||||
Longitude = longitude;
|
||||
}
|
||||
|
||||
public double Latitude { get; set; }
|
||||
|
||||
public double Longitude { get; set; }
|
||||
|
||||
public static LatLong Interpolate(LatLong start, LatLong end, double proportion)
|
||||
{
|
||||
// The Earth is flat, right? So no need for spherical interpolation.
|
||||
return new LatLong(
|
||||
start.Latitude + (end.Latitude - start.Latitude) * proportion,
|
||||
start.Longitude + (end.Longitude - start.Longitude) * proportion);
|
||||
}
|
||||
}
|
||||
}
|
17
BlazingPizza.Shared/NotificationSubscription.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class NotificationSubscription
|
||||
{
|
||||
public int NotificationSubscriptionId { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
|
||||
public string P256dh { get; set; }
|
||||
|
||||
public string Auth { get; set; }
|
||||
}
|
||||
}
|
25
BlazingPizza.Shared/Order.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class Order
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
|
||||
public DateTime CreatedTime { get; set; }
|
||||
|
||||
public Address DeliveryAddress { get; set; } = new Address();
|
||||
|
||||
public LatLong DeliveryLocation { get; set; }
|
||||
|
||||
public List<Pizza> Pizzas { get; set; } = new List<Pizza>();
|
||||
|
||||
public decimal GetTotalPrice() => Pizzas.Sum(p => p.GetTotalPrice());
|
||||
|
||||
public string GetFormattedTotalPrice() => GetTotalPrice().ToString("0.00");
|
||||
}
|
||||
}
|
83
BlazingPizza.Shared/OrderWithStatus.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using BlazingPizza.ComponentsLibrary.Map;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class OrderWithStatus
|
||||
{
|
||||
private static readonly string deliverdStatus = "Geliefert";
|
||||
private static readonly string preparingStatus = "Vorbereiten";
|
||||
private static readonly string deliveringStatus = "Wird geliefert";
|
||||
|
||||
public readonly static TimeSpan PreparationDuration = TimeSpan.FromSeconds(10);
|
||||
public readonly static TimeSpan DeliveryDuration = TimeSpan.FromMinutes(1); // Unrealistic, but more interesting to watch
|
||||
|
||||
public Order Order { get; set; }
|
||||
|
||||
public string StatusText { get; set; }
|
||||
|
||||
public bool IsDelivered => StatusText == deliverdStatus;
|
||||
|
||||
public List<Marker> MapMarkers { get; set; }
|
||||
|
||||
public static OrderWithStatus FromOrder(Order order)
|
||||
{
|
||||
// To simulate a real backend process, we fake status updates based on the amount
|
||||
// of time since the order was placed
|
||||
string statusText;
|
||||
List<Marker> mapMarkers;
|
||||
var dispatchTime = order.CreatedTime.Add(PreparationDuration);
|
||||
|
||||
if (DateTime.Now < dispatchTime)
|
||||
{
|
||||
statusText = preparingStatus;
|
||||
mapMarkers = new List<Marker>
|
||||
{
|
||||
ToMapMarker("Du", order.DeliveryLocation, showPopup: true)
|
||||
};
|
||||
}
|
||||
else if (DateTime.Now < dispatchTime + DeliveryDuration)
|
||||
{
|
||||
statusText = deliveringStatus;
|
||||
|
||||
var startPosition = ComputeStartPosition(order);
|
||||
var proportionOfDeliveryCompleted = Math.Min(1, (DateTime.Now - dispatchTime).TotalMilliseconds / DeliveryDuration.TotalMilliseconds);
|
||||
var driverPosition = LatLong.Interpolate(startPosition, order.DeliveryLocation, proportionOfDeliveryCompleted);
|
||||
mapMarkers = new List<Marker>
|
||||
{
|
||||
ToMapMarker("Du", order.DeliveryLocation),
|
||||
ToMapMarker("Fahrer", driverPosition, showPopup: true),
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = deliverdStatus;
|
||||
mapMarkers = new List<Marker>
|
||||
{
|
||||
ToMapMarker("Lieferaddresse", order.DeliveryLocation, showPopup: true),
|
||||
};
|
||||
}
|
||||
|
||||
return new OrderWithStatus
|
||||
{
|
||||
Order = order,
|
||||
StatusText = statusText,
|
||||
MapMarkers = mapMarkers,
|
||||
};
|
||||
}
|
||||
|
||||
private static LatLong ComputeStartPosition(Order order)
|
||||
{
|
||||
// Random but deterministic based on order ID
|
||||
var rng = new Random(order.OrderId);
|
||||
var distance = 0.01 + rng.NextDouble() * 0.02;
|
||||
var angle = rng.NextDouble() * Math.PI * 2;
|
||||
var offset = (distance * Math.Cos(angle), distance * Math.Sin(angle));
|
||||
return new LatLong(order.DeliveryLocation.Latitude + offset.Item1, order.DeliveryLocation.Longitude + offset.Item2);
|
||||
}
|
||||
|
||||
static Marker ToMapMarker(string description, LatLong coords, bool showPopup = false)
|
||||
=> new Marker { Description = description, X = coords.Longitude, Y = coords.Latitude, ShowPopup = showPopup };
|
||||
}
|
||||
}
|
42
BlazingPizza.Shared/Pizza.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace BlazingPizza
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a customized pizza as part of an order
|
||||
/// </summary>
|
||||
public class Pizza
|
||||
{
|
||||
public const int DefaultSize = 12;
|
||||
public const int MinimumSize = 9;
|
||||
public const int MaximumSize = 17;
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public PizzaSpecial Special { get; set; }
|
||||
|
||||
public int SpecialId { get; set; }
|
||||
|
||||
public int Size { get; set; }
|
||||
|
||||
public List<PizzaTopping> Toppings { get; set; }
|
||||
|
||||
public decimal GetBasePrice()
|
||||
{
|
||||
return ((decimal)Size / (decimal)DefaultSize) * Special.BasePrice;
|
||||
}
|
||||
|
||||
public decimal GetTotalPrice()
|
||||
{
|
||||
return GetBasePrice() + Toppings.Sum(t => t.Topping.Price);
|
||||
}
|
||||
|
||||
public string GetFormattedTotalPrice()
|
||||
{
|
||||
return GetTotalPrice().ToString("0.00");
|
||||
}
|
||||
}
|
||||
}
|
20
BlazingPizza.Shared/PizzaSpecial.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace BlazingPizza
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a pre-configured template for a pizza a user can order
|
||||
/// </summary>
|
||||
public class PizzaSpecial
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public decimal BasePrice { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public string GetFormattedBasePrice() => BasePrice.ToString("0.00");
|
||||
}
|
||||
}
|
11
BlazingPizza.Shared/PizzaTopping.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class PizzaTopping
|
||||
{
|
||||
public Topping Topping { get; set; }
|
||||
|
||||
public int ToppingId { get; set; }
|
||||
|
||||
public int PizzaId { get; set; }
|
||||
}
|
||||
}
|
14
BlazingPizza.Shared/Topping.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class Topping
|
||||
{
|
||||
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public decimal Price { get; set; }
|
||||
|
||||
public string GetFormattedPrice() => Price.ToString("0.00");
|
||||
}
|
||||
}
|
9
BlazingPizza.Shared/UserInfo.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace BlazingPizza
|
||||
{
|
||||
public class UserInfo
|
||||
{
|
||||
public bool IsAuthenticated { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
92
BlazingPizza.sln
Normal file
@ -0,0 +1,92 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.28902.138
|
||||
MinimumVisualStudioVersion = 15.0.26124.0
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazingPizza.Server", "BlazingPizza.Server\BlazingPizza.Server.csproj", "{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazingPizza.Client", "BlazingPizza.Client\BlazingPizza.Client.csproj", "{C33040F6-6F46-4AA2-8A8D-A97C934A0856}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazingPizza.Shared", "BlazingPizza.Shared\BlazingPizza.Shared.csproj", "{42410116-06C1-4D19-B5FC-BD1F85F918B3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazingPizza.ComponentsLibrary", "BlazingPizza.ComponentsLibrary\BlazingPizza.ComponentsLibrary.csproj", "{0C421DC9-9F9A-4C97-9BC5-D959E8840864}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazingComponents", "BlazingComponents\BlazingComponents.csproj", "{4C4132E7-D096-42DA-9C3D-C444B42C3889}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{29F85A53-A43B-4B5F-8C11-4FC62CDF19D6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C33040F6-6F46-4AA2-8A8D-A97C934A0856}.Release|x86.Build.0 = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{42410116-06C1-4D19-B5FC-BD1F85F918B3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0C421DC9-9F9A-4C97-9BC5-D959E8840864}.Release|x86.Build.0 = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|x64.Build.0 = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{4C4132E7-D096-42DA-9C3D-C444B42C3889}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {9574C5F7-86D1-4384-85AC-EA7D09B201AD}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
8
Directory.Build.props
Normal file
@ -0,0 +1,8 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<AspNetCoreVersion>3.1.4</AspNetCoreVersion>
|
||||
<BlazorVersion>3.2.0</BlazorVersion>
|
||||
<EntityFrameworkVersion>3.1.4</EntityFrameworkVersion>
|
||||
<SystemNetHttpJsonVersion>3.2.0</SystemNetHttpJsonVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app/
|
||||
RUN dotnet publish -c Release -o out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/out ./
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["dotnet", "BlazingPizza.Server.dll"]
|