Add project files.

This commit is contained in:
Josh Deck 2026-03-24 10:24:18 -04:00
parent f8dd5ac23b
commit 4a30fa07a2
82 changed files with 2149 additions and 0 deletions

View File

@ -0,0 +1,5 @@
{
"version": 1,
"isRoot": true,
"tools": {}
}

View File

@ -0,0 +1,7 @@
@* @using static Microsoft.AspNetCore.Components.Web.RenderMode
@rendermode InteractiveServer
*@
<CascadingValue Value="this">
@ChildContent
</CascadingValue>

View File

@ -0,0 +1,156 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using System.Reflection;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace SummerBestWebForm2.AppState;
public partial class CascadingAppState : ComponentBase, IAppState
{
private readonly string StorageKey = "SummerBestEnrollment-KeyMotive";
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Inject]
ProtectedLocalStorage? localStorage { get; set; } = default!;
//[Inject]
//IHttpContextAccessor? httpContextAccessor { get; set; } = default!;
[CascadingParameter] HttpContext? httpContext { get; set; } = default!;
bool isLoaded = false;
public Guid SessionId { get; set; } = Guid.Empty;
public bool isInit { get; set; } = false;
//public CascadingAppState()
//{
// Load();
//}
private string? _myIpAddress = string.Empty;
public string? myIpAddress
{
get => _myIpAddress;
set
{
_myIpAddress = value;
Save();
}
}
private DateTimeOffset _DateCreated = DateTimeOffset.Now;
public DateTimeOffset DateCreated
{
get => _DateCreated;
set
{
_DateCreated = value;
Save();
}
}
private DateTimeOffset _DateExpires = DateTimeOffset.Now;
public DateTimeOffset DateExpires
{
get => _DateExpires;
set
{
_DateExpires = value;
Save();
}
}
// (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O) / (o) (O)
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await LoadAsync();
StateHasChanged();
}
}
private void Save()
{
new Task(async () =>
{
await SaveAsync();
}).Start();
} //Save
public async Task SaveAsync()
{
if (!isLoaded) return;
// serialize
var state = (IAppState)this;
var json = JsonSerializer.Serialize(state);
// save
await localStorage.SetAsync(StorageKey, json);
} //SaveAsync
private void Load()
{
new Task(async () =>
{
await LoadAsync();
}).Start();
} //Load
public async Task LoadAsync()
{
string remoteIpAddr = string.Empty;
try
{
//remoteIpAddr = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
remoteIpAddr = this.httpContext?.Connection.RemoteIpAddress?.ToString() ?? "Not Set";
var data = await localStorage.GetAsync<string>(StorageKey);
var state = JsonSerializer.Deserialize<MdlAppState>(data.Value);
if (state != null)
{
if (DateTimeOffset.Now < state.DateExpires)
{
// decide whether to set properties manually or with reflection
// comment to set properties manually
//this.Message = state.Message;
//this.Count = state.Count;
// set properties using Reflaction
var t = typeof(IAppState);
//var props = t.GetProperties();
PropertyInfo[] props = t.GetProperties();
foreach (var prop in props)
{
//if (!Regex.IsMatch(prop.Name, "isInit|SessionId|"))
{
var value = prop.GetValue(state, null);
prop.SetValue(this, value, null);
}
}
}
}
}
catch (Exception ex)
{
// do something
}
isLoaded = true;
DateExpires = DateTimeOffset.Now.AddHours(36);
myIpAddress = remoteIpAddr;
if (!isInit)
{
DateCreated = DateTimeOffset.Now;
SessionId = Guid.NewGuid();
isInit = true;
await SaveAsync();
}
await InvokeAsync(() =>
{
StateHasChanged();
});
} //LoadAsync
}

10
AppState/IAppState.cs Normal file
View File

@ -0,0 +1,10 @@
namespace SummerBestWebForm2.AppState;
public interface IAppState
{
bool isInit { get; set; }
string? myIpAddress { get; set; }
Guid SessionId { get; set; }
DateTimeOffset DateCreated { get; set; }
DateTimeOffset DateExpires { get; set; }
}

10
AppState/MdlAppState.cs Normal file
View File

@ -0,0 +1,10 @@
namespace SummerBestWebForm2.AppState;
public class MdlAppState : IAppState
{
public bool isInit { get; set; }
public string? myIpAddress { get; set; }
public Guid SessionId { get; set; }
public DateTimeOffset DateCreated { get; set; }
public DateTimeOffset DateExpires { get; set; }
}

View File

@ -0,0 +1,8 @@
namespace SummerBestWebForm2.ClassObj;
public class IPAddressService
{
public const string TokenName = "IPAddress";
public string RemoteIpAddress { get; set; } = "Not Set";
}

View File

@ -0,0 +1,15 @@
namespace SummerBestWebForm2;
public class MdlSessionInfo
{
public const string KEY_IPADDRESS = "IPAddress";
public string IPAddress { get; set; } = null!;
public bool isInit { get; set; } = false;
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.MinValue;
public MdlSessionInfo()
{
// Have to have a blank ctor here for ProtectedSessionStorage
}
}

123
Components/App.razor Normal file
View File

@ -0,0 +1,123 @@
@implements IDisposable
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<meta name="description" content="KeyMotive's Summer Best Customer Program Enrollment" />
<meta name="author" content="KeyMotive LLC" />
<meta name="Charset" content="US-ASCII" />
<meta name="Distribution" content="Global" />
<meta name="Rating" content="General" />
<meta name="Robots" content="INDEX,FOLLOW" />
<meta name="Revisit-after" content="14 Days" />
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin />
<link href='https://fonts.googleapis.com/css?family=Roboto|Handlee|Candal|Exo 2|Armata|Poppins|Kalam:wght@300&display=swap' rel='stylesheet' type='text/css' />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" integrity="sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="SummerBestWebForm2.styles.css" />
<link rel="icon" type="image/png" sizes="512x512" href="/img/favico-512.png">
<link rel="icon" type="image/png" sizes="256x256" href="/img/favico-256.png">
<link rel="icon" type="image/png" sizes="128x128" href="/img/favico-128.png">
<link rel="icon" type="image/png" sizes="64x64" href="/img/favico-64.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favico-32.png">
<style type="text/css">
div.container {
background-color: #fff;
min-height: 100vh;
}
</style>
@* <HeadOutlet @rendermode="@(new InteractiveServerRenderMode(false))" /> *@
<HeadOutlet @rendermode="InteractiveServer" />
<link rel="stylesheet" href="https://blazor.cdn.telerik.com/blazor/6.2.0/kendo-theme-bootstrap/swatches/bootstrap-urban.css" />
<script src="https://blazor.cdn.telerik.com/blazor/6.2.0/telerik-blazor.min.js" defer></script>
</head>
<body>
<!-- Turn off pre-rendering -->
@* <Routes @rendermode="@(new InteractiveServerRenderMode(false))" /> *@
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/js/all.min.js" integrity="sha512-u3fPA7V8qQmhBPNT5quvaXVa1mnnLSXUep5PS1qo5NRzHwG19aHmNJnj1Q8hpA/nBWZtZD4r4AX6YOt5ynLN2g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
window.WriteCookie = {
WriteCookie: function (name, value, days) {
var expires;
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toGMTString();
}
else {
expires = "";
}
document.cookie = name + "=" + value + expires + "; path=/";
}
}
window.ReadCookie = {
ReadCookie: function (cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
}
</script>
<pre>@errMsg</pre>
@* @if (appState != null)
{
<pre style="font-size:.8em">
SessionId: @appState.SessionId
DateCreated: @appState.DateCreated
DateExpires: @appState.DateExpires
IPAddress: @appState.myIpAddress
</pre>
} *@
</body>
</html>
@code {
[CascadingParameter]
public CascadingAppState appState { get; set; }
#region "IPADDR"
[CascadingParameter] HttpContext? HttpContext { get; set; }
[Inject] public PersistentComponentState ApplicationState { get; set; } = default!;
private PersistingComponentStateSubscription? _persistingSubscription;
//private bool _subsequentRender;
private string RemoteIpAddress = "Not Set";
protected override void OnInitialized()
{
this.RemoteIpAddress = this.HttpContext?.Connection.RemoteIpAddress?.ToString() ?? "Not Set";
_persistingSubscription = ApplicationState.RegisterOnPersisting(this.PersistData);
}
public Task PersistData()
{
this.ApplicationState.PersistAsJson<string>(ClassObj.IPAddressService.TokenName, this.RemoteIpAddress);
return Task.CompletedTask;
}
void IDisposable.Dispose()
{
_persistingSubscription?.Dispose();
}
#endregion
private string errMsg = string.Empty;
}

View File

@ -0,0 +1,29 @@
@using ClassObj
Welcome, @RemoteIpAddress!
@code {
[Inject] public IPAddressService IPAddressService { get; set; } = default!;
[Inject] public PersistentComponentState ApplicationState { get; set; } = default!;
private bool _subsequentRender;
private const string TokenName = "IPAddress";
private string RemoteIpAddress = "Not Set";
// Short circuit all the lifecycle stuff - we don't need it
public override Task SetParametersAsync(ParameterView parameters)
{
if (_subsequentRender)
return Task.CompletedTask;
// if not prerender, try and get the persisted value
if (this.ApplicationState.TryTakeFromJson<string>(IPAddressService.TokenName, out var address))
{
this.RemoteIpAddress = address ?? "Not Set";
this.IPAddressService.RemoteIpAddress = this.RemoteIpAddress;
}
_subsequentRender = true;
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,5 @@
@inherits LayoutComponentBase
<TelerikRootComponent>
@Body
</TelerikRootComponent>

View File

@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
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;
}

View File

@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">SummerBestWebForm2</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@ -0,0 +1,18 @@
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code {
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

546
Components/Pages/Home.razor Normal file
View File

@ -0,0 +1,546 @@
@rendermode InteractiveServer
@inject PersistentComponentState ApplicationState
@inject ClassObj.IPAddressService IpAddressService
@page "/"
<PageTitle>Welcome</PageTitle>
@if (ProgramClosed)
{
<div class="container">
<div class="row">
<div class="col-12 text-center mt-5">
<h1>Thank you for your interest!</h1>
<p>Unfortunately, the enrollment period for our Summer Growth Campaign has drawn to a close.</p>
<p>Stay tuned for more great campaigns in the future!</p>
<p><i class="fa-solid fa-heart"></i></p>
</div>
<div class="col-12 text-center">
<img class="img-fluid" src="/img/34d1685e-d655-421b-aef8-e077140534f9.png" alt="Thank You!" />
</div>
</div>
</div>
}
else if (ShowWizard)
{
<div class="container">
<div class="row">
<div class="col-12">
<TelerikWizard @bind-Value="@Value" OnFinish="OnFinishHandler" Width="100%" Height="95vh" Class="sbWizard" StepperPosition=WizardStepperPosition.Top>
<WizardSteps>
<WizardStep Label="Introduction" Icon="@SvgIcon.Globe" OnChange="@OnAudienceChoiceStepChange" Valid="@IsAudienceChoiceValid">
<Content>
<TelerikForm Model="@cardType" @ref="@IntroForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
<div class="col-12 px-sm-5 mb-2">
<h1>KeyMotive's Summer Growth Program</h1>
<div class="px-sm-5 mb-2">
<p>It's time for our Summer Growth Program!<br />If you're looking to kick off your summer season with a wildly successful campgaign, then you've come to the right place!</p>
<p>
Using this wizard, choose your target audience, decide what media you'd like to mail, choose from a few suggested designs (or request a custom design from our
creative team), and that's it!
</p>
<p>Let us handle the rest!</p>
</div>
<div>
<img src="/img/7ccdfcc7-f91b-497e-8c19-ebb3a4021ea3.png" class="img-fluid" />
</div>
<div class="px-sm-5 mb-2 text-start">
Your audience choices are:
<ul>
<li>A list of your <strong class="text-primary">Very Best Customers</strong> (always a heavy-hitting campaign!)</li>
<li>
A list composed of <strong class="text-primary">Great Prospects</strong> near your shop, as well as a mix of formerly-Great Customers that you
<strong class="text-primary">haven't seen during the past 8 months</strong>.
</li>
</ul>
</div>
<div class="px-sm-5 text-start">
<strong>Please Select:</strong>
<TelerikRadioGroup Data="@audiences"
@bind-Value="@audienceType"
ValueField="@nameof(AudienceType.AudienceDescription)"
TextField="@nameof(AudienceType.AudienceDescription)">
</TelerikRadioGroup>
</div>
</div>
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons>
@* Need this here to avoid addition of random submit button *@
</FormButtons>
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 1 - plastic card vs postcard *@
@* *@
<WizardStep Label="Card Type" Icon="@SvgIcon.EnvelopeBox" OnChange="@OnCardChoiceStepChange" Valid="@IsCardChoiceValid">
<Content>
<TelerikForm Model="@cardType" @ref="@cardTypeForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="text-center">Step 1 - Choose a Card Type</h1>
</div>
<div class="col-12 text-center mb-3">
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single">
<ButtonGroupToggleButton @bind-Selected="@isPlasticCard">Plastic Card</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@isPostcard">Postcard</ButtonGroupToggleButton>
</TelerikButtonGroup>
</div>
@* Preview information about the selected card type*@
@if (isPlasticCard)
{
<div class="col-sm-4 mb-3 text-start">
<h4>Very Popular Plastic Card Mailer!</h4>
<ul>
<li>As thick as a typical credit card!</li>
<li>Large size piece (8&frac14; x 4 inches)</li>
<li>Coupons pop out, offering great convenience for your customers!</li>
<li>Leave it to us - we select, print, sort and mail for just <strong class="text-primary text-nowrap">$1.05 each</strong></li>
</ul>
</div>
<div class="col-sm-8 text-center mb-3">
<img class="img-fluid" src="/img/1181c4aa-4f3f-4eb4-8d5d-7eff0131d5f2.png" />
</div>
}
else
{
<div class="col-sm-4 mb-3 text-start">
<h4>Premium Postcard Really Stands Out in the Mailbox</h4>
<ul>
<li>Premium paper, Jumbo-Sized (8&frac12; x 6 inches)</li>
<li>UV gloss-coated</li>
<li>Choose from our selection, or ask for a custom design!</li>
<li>Full service - select, print, sort, mail for just <strong class="text-primary text-nowrap">82&#162; each</strong></li>
</ul>
</div>
<div class="col-sm-8 text-center mb-3">
<img class="img-fluid" src="/img/ad5a2b69-a493-429b-9edd-8320ec1e9c33.png" />
</div>
}
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons>
@* Need this here to avoid addition of random submit button *@
</FormButtons>
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 2 - selecting card design *@
@* *@
<WizardStep Label="Card Design" Icon="@SvgIcon.MapMarkerTarget" OnChange="@OnDesignStepChange" Valid="@IsDesignChoiceValid">
<Content>
<TelerikForm Model="@custOptions" @ref="@customizationForm">
<FormValidation>
<DataAnnotationsValidator></DataAnnotationsValidator>
</FormValidation>
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
<div class="col-12">
<h1 class="text-center">Choose a Design</h1>
</div>
<div class="col-12 text-center mb-3">
@* Button logic *@
<div class="text-center">
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single">
@if (isPlasticCard)
{
<ButtonGroupToggleButton @bind-Selected="@designOne">Plastic Card 1</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designTwo">Plastic Card 2</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designThree">Plastic Card 3</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designCustom">Custom Design</ButtonGroupToggleButton>
}
else
{
<ButtonGroupToggleButton @bind-Selected="@designOne">Postcard 1</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designTwo">Postcard 2</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designThree">Postcard 3</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@designCustom">Custom Design</ButtonGroupToggleButton>
}
</TelerikButtonGroup>
<br /><br />
@* Card preview logic *@
@if (designOne)
{
@if (isPostcard)
{
<img src="/img/Postcard1_Front.png" class="img-fluid" alt="Back" />
<img src="/img/Postcard1_Back.png" class="img-fluid" alt="Front" />
}
else
{
<img src="/img/Plastic1_Back.png" class="img-fluid" alt="Back" />
<img src="/img/Plastic1_Front.png" class="img-fluid" alt="Front" />
}
}
else if (designTwo)
{
@if (isPostcard)
{
<img src="/img/Postcard2_Front.png" class="img-fluid" alt="Back" />
<img src="/img/Postcard2_Back.png" class="img-fluid" alt="Front" />
}
else
{
<img src="/img/Plastic2_Back.png" class="img-fluid" alt="Back" />
<img src="/img/Plastic2_Front.png" class="img-fluid" alt="Front" />
}
}
else if (designThree)
{
@if (isPostcard)
{
<img src="/img/Postcard3_Front.png" class="img-fluid" alt="Back" />
<img src="/img/Postcard3_Back.png" class="img-fluid" alt="Front" />
}
else
{
<img src="/img/Plastic3_Back.png" class="img-fluid" alt="Back" />
<img src="/img/Plastic3_Front.png" class="img-fluid" alt="Front" />
}
}
else if (designCustom)
{
<div class="d-sm-flex justify-content-around">
<div class="px-5">
<h4 class="mb-5">If you have your own idea in mind, or you would like to try another design, our creative design team will make it happen!</h4>
</div>
<div class="px-5">
<p class="fst-italic">
When you're asked for "additional comments," just give us an idea of what you have in mind and our customer care team will have some ideas
ready when we call you!
</p>
</div>
</div>
<div>
@if (isPostcard)
{
<img src="/img/cfeb51c5-5373-44b5-be19-dadea452cc41.png" class="img-fluid" />
}
else
{
<img src="/img/a41f648d-6b39-41f1-bbe5-084fb8a71a30.png" class="img-fluid" />
}
</div>
}
</div>
</div>
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons />
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 3 - For postcards, choose a verse and a signature *@
@* *@
<WizardStep Label="Customization" OnChange="@OnMessagingStepChange" Valid="@isMessagingValid">
<Content>
<TelerikForm Model="@messagingOptions" @ref="@messagingForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
@if (isPlasticCard)
{
<div class="col-12">
<h4>No customization needed. Please proceed to logo selection.</h4>
</div>
}
else
{
<div class="col-12">
<h4 class="mb-3">Please Select a Verse</h4>
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single" Class="vertical-buttons block-buttons mb-5">
<ButtonGroupToggleButton @bind-Selected="@verseOne">Option 1</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@verseTwo">Option 2</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@verseThree">Option 3</ButtonGroupToggleButton>
</TelerikButtonGroup>
@if (verseOne)
{
<p class="fst-italic">Thank you for making our success possible.</p>
<p class="fst-italic">We appreciate loyal customers like you and look forward to continuing to be your complete auto repair, service and tire center!</p>
}
else if (verseTwo)
{
<p class="fst-italic">We appreciate the trust you have shown in us and look forward to working with you in the future!</p>
}
else if (verseThree)
{
<p class="fst-italic">We want you to know that we truly appreciate your business and will make every effort possible to continue to provide you with excellent car care.</p>
}
</div>
<div class="col-12">
<hr class="m-5" />
<h4 class="mb-3">Please Select a Signature</h4>
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single" Class="mb-5">
<ButtonGroupToggleButton @bind-Selected="@sigOne">From Your Friends</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@sigTwo">Name, Title, Phone Number</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@sigThree">Custom</ButtonGroupToggleButton>
</TelerikButtonGroup>
@if (sigOne)
{
<p>From your friends at <strong>TopSpeed Tire</strong><br /><i>(your location name)</i></p>
<p><strong>(888) 555-3712</strong><br /><i>(your location phone number)</i></p>
}
else if (sigTwo)
{
<label style="display:block" for="CustomName" class="k-label k-form-label text-center">Name:</label>
<TelerikTextBox Class="mb-3" Id="CustomName" @bind-Value="@customName" MaxLength="50" Width="25rem"></TelerikTextBox>
<label style="display:block" for="CustomTitle" class="k-label k-form-label text-center">Title:</label>
<TelerikTextBox Class="mb-3" Id="CustomTitle" @bind-Value="@customTitle" MaxLength="50" Width="25rem"></TelerikTextBox>
<label style="display:block" for="CustomPhone" class="k-label k-form-label text-center">Phone:</label>
<TelerikTextBox Class="mb-3" Id="CustomPhone" @bind-Value="@customPhone" MaxLength="15" Width="20rem"></TelerikTextBox>
}
else if (sigThree)
{
<br />
<label style="display:block" for="CustomSignature" class="text-center">Custom Message (max 30 character/line up to 4 lines)</label>
<br />
<TelerikTextArea Id="CustomSignature" @bind-Value="@customSignature" Rows="4" Cols="35" MaxLength="120"></TelerikTextArea>
}
</div>
}
<div class="col-12 text-center mb-3">
</div>
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons />
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 4 - Logo Selection *@
@* *@
<WizardStep Label="Logos" OnChange="@OnLogoStepChange" Valid="@isLogoValid">
<Content>
<TelerikForm Model="@logoOptions"
@ref="@logoForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
<div class="col-12">
<TelerikCheckBox Id="goodyearBox" @bind-Value="@goodyear" />
<label for="goodyearBox">Goodyear Logo</label>
<TelerikCheckBox Id="michelinBox" @bind-Value="@michelin" />
<label for="michelinBox">Michelin Logo</label>
<TelerikCheckBox Id="customBox" @bind-Value="@custom" />
<label for="customBox">Custom Logo</label>
</div>
<div class="col-12 mt-5">
@if (goodyear)
{
<div class="p-5">
<img src="/img/goodyear.png" class="img-fluid" />
</div>
}
@if (michelin)
{
<div class="p-5">
<img src="/img/michelin.png" class="img-fluid" />
</div>
}
@if (custom)
{
<div class="p-5">
<img src="/img/6784cef1-9ed9-4c91-94bc-5e7a267b5a4f.png" class="img-fluid" />
</div>
}
</div>
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons>
</FormButtons>
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 5 - Offer selection for Plastic cards *@
@* *@
<WizardStep Label="Offers" OnChange="@OnOfferStepChange" Valid="@isOfferSelectionValid">
<Content>
<TelerikForm Model="@offerOptions" @ref="@offerForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
@if (isPlasticCard)
{
<div class="col-12">
<h4 class="my-3">Please select two (2) full size offers:</h4>
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Multiple">
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[0]"><img src="/img/A1.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[1]"><img src="/img/A2.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[2]"><img src="/img/A3.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[3]"><img src="/img/A4.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[4]"><img src="/img/A5.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@bigOffers[5]"><img src="/img/A6.png" /></ButtonGroupToggleButton>
</TelerikButtonGroup>
</div>
<div class="col-12">
<h4 class="my-3">And four (4) half-sized offers:</h4>
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Multiple">
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[0]"><img src="/img/B1.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[1]"><img src="/img/B2.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[2]"><img src="/img/B3.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[3]"><img src="/img/B4.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[4]"><img src="/img/B5.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[5]"><img src="/img/B6.png" /></ButtonGroupToggleButton>
</TelerikButtonGroup>
<br />
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Multiple">
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[6]"><img src="/img/B7.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[7]"><img src="/img/B8.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[8]"><img src="/img/B9.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[9]"><img src="/img/B10.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[10]"><img src="/img/B11.png" /></ButtonGroupToggleButton>
<ButtonGroupToggleButton Size="@ButtonSize" @bind-Selected="@smallOffers[11]"><img src="/img/B12.png" /></ButtonGroupToggleButton>
</TelerikButtonGroup>
</div>
}
else
{
<div class="col-12 m-5">
<h4>No offers needed! Please proceed to the next step!</h4>
</div>
}
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons />
</TelerikForm>
</Content>
</WizardStep>
@* *@
@* Step 6 - Location information *@
@* *@
<WizardStep Label="Location Info" OnChange="@OnLocationStepChange" Valid="@isLocationInfoValid">
<Content>
<div class="container">
<div class="row">
<div class="col-12">
<TelerikForm Model="@locationInfo" @ref="@locationForm" Columns="2" ColumnSpacing="25px">
<FormButtons />
</TelerikForm>
</div>
</div>
</div>
</Content>
</WizardStep>
@* *@
@* Step 7 - Payment *@
@* *@
<WizardStep Label="Payment Info">
<Content>
<TelerikForm Model="@paymentInfo" @ref="@paymentForm">
<FormItems>
<FormItem>
<Template>
<div class="container">
<div class="row">
<div class="col-12">
<TelerikButtonGroup SelectionMode="@ButtonGroupSelectionMode.Single" Class="vertical-buttons block-buttons mb-5">
<ButtonGroupToggleButton @bind-Selected="@ccOnFile">Credit Card On File</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@callWithInfo">Call With Payment Info</ButtonGroupToggleButton>
<ButtonGroupToggleButton @bind-Selected="@check">Check</ButtonGroupToggleButton>
</TelerikButtonGroup>
</div>
<div class="col-12 mb-5">
@if (ccOnFile)
{
<h4>Use our preferred payment method already on file.</h4>
}
else if (callWithInfo)
{
<h4>Please call me.</h4>
<p>I'll specify method of payment when reviewing the order.</p>
}
else if (check)
{
<h4>I'll be sending in a check</h4>
<p>I'll be sending in a check by the invoice's due date (6/13/2025).</p>
}
</div>
<div class="col-12">
<label style="display:block" for="RequestedQuantity" class="text-center mb-3">Requested Quantity</label>
<TelerikTextBox class="mb-3" Id="RequestedQuantity" @bind-Value="@requestedQuantity" MaxLength="6" Width="15rem"></TelerikTextBox>
<label style="display:block" for="AdditionalComments" class="text-center mb-3">Additional Notes</label>
<TelerikTextArea class="mb-3" Id="CustomSignature" @bind-Value="@additionalComments" Rows="8" Cols="100" MaxLength="2500"></TelerikTextArea>
</div>
</div>
</div>
</Template>
</FormItem>
</FormItems>
<FormButtons />
</TelerikForm>
</Content>
</WizardStep>
</WizardSteps>
</TelerikWizard>
</div>
</div>
</div>
}
else
{
<div class="container">
<div class="row">
<div class="col-12 m-5">
Thank you, our customer care team will be in touch with you soon!
</div>
<div class="col-12">
<RandomImage />
</div>
</div>
</div>
}
@* @if (appState != null)
{
<pre style="font-size:.8em">
SessionId: @appState.SessionId
DateCreated: @appState.DateCreated
DateExpires: @appState.DateExpires
IPAddress: @appState.myIpAddress
IPAddress2: @(IpAddressService.RemoteIpAddress.ToString())
</pre>
}
*@

View File

@ -0,0 +1,403 @@
using Microsoft.AspNetCore.Components;
using System.ComponentModel.DataAnnotations;
using Telerik.Blazor;
using Telerik.Blazor.Components;
using Telerik.SvgIcons;
using kmCommonLibsCore;
using System.Net.Http;
using System.Text.Json;
using Microsoft.Extensions.Options;
using SummerBestWebForm2.AppState;
using SummerBestWebForm2.ClassObj;
namespace SummerBestWebForm2.Components.Pages;
public partial class Home
{
[CascadingParameter]
public CascadingAppState appState { get; set; } = default!;
async Task OnFinishHandler()
//private void SaveIt()
{
using (var em = new kmCommonLibsCore.Emails() { HandleOptOuts = false, SendMethod = enuSendMethod.OnsiteServer })
{
// Parsing the submitted form to pull the relevant information
var cardType = isPostcard ? "Postcard" : "Plastic Card";
var cardDesign = string.Empty;
if (designOne)
cardDesign = "A";
else if (designTwo)
cardDesign = "B";
else if (designThree)
cardDesign = "C";
else if (designCustom)
cardDesign = "CUSTOM";
// Postcard or Plastic specific options
var customizationInfo = string.Empty;
if (isPostcard)
{
var verseChoice = string.Empty;
var sigChoice = string.Empty;
if (verseOne)
verseChoice = "1";
else if (verseTwo)
verseChoice = "2";
else if (verseThree)
verseChoice = "3";
if (sigOne)
sigChoice = string.Format("Option D - From Your Friends At:<br />{0}<br />{1}", locationInfo.LocationName, locationInfo.PhoneNumber);
else if (sigTwo)
sigChoice = string.Format("Option E - Name: {0}<br />Title: {1}<br />Phone: {2}", customName, customTitle, customPhone);
else if (sigThree)
sigChoice = string.Format("Option F - {0}", customSignature);
// combine these into customizationInfo
customizationInfo = string.Format("<tr><td>Verse:</td><td>{0}</td></tr><tr><td>Signature:</td><td>{1}</td></tr>", verseChoice, sigChoice);
}
else // isPlasticCard
{
var smallOfferList = new List<string>();
var bigOfferList = new List<string>();
for (int index = 0; index < smallOffers.Length; index++)
{
if (smallOffers[index])
smallOfferList.Add(string.Format("B{0}", index + 1));
}
for (int index = 0; index < bigOffers.Length; index++)
{
if (bigOffers[index])
bigOfferList.Add(string.Format("A{0}", index + 1));
}
customizationInfo = string.Format("<tr><td>BIG Offers:</td><td>{0}</td></tr><tr><td>SMALL Offers:</td><td>{1}</td></tr>",
string.Join(", ", bigOfferList), string.Join(", ", smallOfferList));
}
// Logo info
var logos = new List<string>();
if (goodyear)
logos.Add("Goodyear");
if (michelin)
logos.Add("Michelin");
if (custom)
logos.Add("Custom");
if (logos.Count == 0)
logos.Add("NONE");
// Payment info
string paymentMethod = string.Empty;
if (ccOnFile)
paymentMethod = "Credit Card on File";
else if (callWithInfo)
paymentMethod = "Call With Payment Info";
else if (check)
paymentMethod = "Check";
// Formulating the email to send
em.Subject = "SBC Order Form Submission";
em.AddAddress(enuAddressType.From, "support@keymotive.us", "Summer Growth Enrollment");
em.AddAddress(enuAddressType.To, "support@keymotive.net", "KeyMotive Support");
em.AddAddress(enuAddressType.CC, "jondeck@keymotive.net", "Jon Deck");
var targetAudience = string.Format("<tr><td>Audience:</td><td>{0}</td></tr>", string.IsNullOrWhiteSpace(audienceType) ? "NOTHING!" : audienceType);
var locInfoString = string.Format("<tr style='padding-top:14px'><td>Location Name:</td><td>{0}</td></tr>" +
"<tr><td>Manager:</td><td>{1}</td></tr>" +
"<tr><td>Address:</td><td>{2}</td></tr>" +
"<tr><td>City:</td><td>{3}</td></tr>" +
"<tr><td>State:</td><td>{4}</td></tr>" +
"<tr><td>Zip:</td><td>{5}</td></tr>" +
"<tr style='padding-top:14px'><td>Phone Number:</td><td>{6}</td></tr>" +
"<tr><td>Contact Name:</td><td>{7}</td></tr>" +
"<tr><td>Contact Phone:</td><td>{8}</td></tr>" +
"<tr><td>Contact Email:</td><td>{9}</td></tr>",
locationInfo.LocationName, locationInfo.Manager, locationInfo.Address, locationInfo.City, locationInfo.State,
locationInfo.Zip, locationInfo.PhoneNumber, locationInfo.ContactName,
locationInfo.ContactPhone, locationInfo.ContactEmail);
var cardInfoString = string.Format("<tr><td>Card type:</td><td>{0}, Design {1}</td></tr>" +
"<tr style='padding-top:14px'><td colspan='2'>Customization Options:</td></tr>" +
"{2}" +
"<tr><td>Logos:</td><td>{3}</td></tr>",
cardType, cardDesign, customizationInfo, string.Join(", ", logos));
if (int.TryParse(requestedQuantity, out _))
requestedQuantity = string.Format("{0:#,##0}", int.Parse(requestedQuantity));
var paymentString = string.Format("<tr><td>Payment Method:</td><td>{0}</td></tr>" +
"<tr style='padding-top:14px'><td>Requested Quantity:</td><td>{1}</td></tr>" +
"<tr><td>Additional Comments:</td><td>{2}</td></tr>", paymentMethod, requestedQuantity, additionalComments);
var ipString = string.Format("<tr style='padding-top:20px'><td>IP Address</td><td>{0}</td></tr>",
string.IsNullOrWhiteSpace(IpAddressService.RemoteIpAddress.ToString()) ? "NONE" : IpAddressService.RemoteIpAddress.ToString());
em.HtmlBody = "<div style='font-size:1.05em'><b>You have a new enrollment:</b><br /><br /><table style='font-size:1.05em;display:block;font-family: monospace;'>" +
targetAudience + locInfoString + cardInfoString + paymentString + ipString + "</table></div>";
try
{
em.Send();
await Console.Out.WriteLineAsync("Done sending");
}
catch (Exception e)
{
await Console.Out.WriteLineAsync("ERROR: " + e.Message);
}
ShowWizard = false;
await Dialogs.AlertAsync("The Registration was submitted successfully", "Done");
}
}
#region "User Selections - Model"
public bool? IsAudienceChoiceValid { get; set; } = false;
public bool? IsCardChoiceValid { get; set; } = false;
public bool? IsDesignChoiceValid { get; set; } = false;
[CascadingParameter]
public DialogFactory Dialogs { get; set; } = default!;
public bool ProgramClosed { get; set; } = false;
public bool ShowWizard { get; set; } = true;
public string ButtonSize { get; set; } = "sm";
public int Value { get; set; } = 0;
//public TelerikForm RegisterForm { get; set; }
public User UserModel { get; set; } = new User();
public TelerikForm IntroForm { get; set; } = new();
// Variables for selecting between plastic and post
public TelerikForm cardTypeForm { get; set; } = new();
public CardType cardType { get; set; } = new CardType();
public bool isPlasticCard { get; set; } = true;
public bool isPostcard { get; set; } = false;
public string audienceType { get; set; } = string.Empty;
public List<AudienceType> audiences { get; set; } = new List<AudienceType>
{
new AudienceType() {AudienceDescription="Send to my BEST CUSTOMERS"},
new AudienceType() {AudienceDescription="Send to Great Prospects as well as Customers who haven't been in during the past 8 months"}
};
// Variables for selecting specific design
public TelerikForm customizationForm { get; set; } = new();
public CustomizationOptions custOptions { get; set; } = new CustomizationOptions();
public bool designOne = true;
public bool designTwo = false;
public bool designThree = false;
public bool designCustom = false;
// Variables for selecting messaging (postcards only)
public TelerikForm messagingForm { get; set; } = new();
public MessagingOptions messagingOptions { get; set; } = new MessagingOptions();
public bool verseOne = true;
public bool verseTwo = false;
public bool verseThree = false;
public bool sigOne = true;
public bool sigTwo = false;
public bool sigThree = false;
public string customName = string.Empty; // For sig two
public string customTitle = string.Empty;
public string customPhone = string.Empty;
public string customSignature = string.Empty; // For sig three
public bool isMessagingValid = false;
// Variables for logo selection
public TelerikForm logoForm { get; set; } = new();
public LogoOptions logoOptions { get; set; } = new LogoOptions();
public bool goodyear = false;
public bool michelin = false;
public bool custom = false;
public bool isLogoValid = false;
// Variables for offer selection (plastic cards only)
public TelerikForm offerForm { get; set; } = new();
public OfferOptions offerOptions { get; set; } = new OfferOptions();
public bool[] bigOffers = new bool[6];
public bool[] smallOffers = new bool[12];
public bool isOfferSelectionValid = false;
// Location information
public TelerikForm locationForm { get; set; } = new();
public LocationInfo locationInfo { get; set; } = new LocationInfo();
public string locationName { get; set; } = string.Empty;
public string manager { get; set; } = string.Empty;
public string address { get; set; } = string.Empty;
public string city { get; set; } = string.Empty;
public string state { get; set; } = string.Empty;
public string zip { get; set; } = string.Empty;
public string phoneNumber { get; set; } = string.Empty;
public string contactName { get; set; } = string.Empty;
public string contactPhone { get; set; } = string.Empty;
public string contactEmail { get; set; } = string.Empty;
public bool isLocationInfoValid { get; set; } = false;
// Payment information
public TelerikForm paymentForm { get; set; } = new();
public PaymentInfo paymentInfo { get; set; } = new PaymentInfo();
public string requestedQuantity = string.Empty;
public string additionalComments = string.Empty;
public bool ccOnFile = true;
public bool callWithInfo = false;
public bool check = false;
#endregion
public void ToggleCardType()
{
isPlasticCard = !isPlasticCard;
isPostcard = !isPostcard;
}
public void OnAudienceChoiceStepChange(WizardStepChangeEventArgs args)
{
IsAudienceChoiceValid = !string.IsNullOrWhiteSpace(audienceType);
}
public void OnCardChoiceStepChange(WizardStepChangeEventArgs args)
{
IsCardChoiceValid = true; // This is forced to be true but required nonetheless
}
public void OnDesignStepChange(WizardStepChangeEventArgs args)
{
IsDesignChoiceValid = true; // Same as card choice
}
public void OnMessagingStepChange(WizardStepChangeEventArgs args)
{
isMessagingValid = true;
}
public void OnLogoStepChange(WizardStepChangeEventArgs arg)
{
isLogoValid = true;
}
public async void OnOfferStepChange(WizardStepChangeEventArgs args)
{
if (isPostcard)
{
isOfferSelectionValid = true;
}
else
{
int bigOfferCount = 0;
foreach (bool selection in bigOffers)
{
if (selection)
bigOfferCount++;
}
int smallOfferCount = 0;
foreach (bool selection in smallOffers)
{
if (selection)
smallOfferCount++;
}
if (smallOfferCount == 4 && bigOfferCount == 2)
isOfferSelectionValid = true;
else
isOfferSelectionValid = false;
if (!isOfferSelectionValid)
{
args.IsCancelled = true;
await Dialogs.AlertAsync("Please select the proper amount of offers.", "You cannot proceed");
}
}
}
public async void OnLocationStepChange(WizardStepChangeEventArgs args)
{
isLocationInfoValid = !string.IsNullOrWhiteSpace(locationInfo.ContactEmail) &&
!string.IsNullOrWhiteSpace(locationInfo.ContactPhone) &&
!string.IsNullOrWhiteSpace(locationInfo.ContactName) &&
!string.IsNullOrWhiteSpace(locationInfo.City) &&
!string.IsNullOrWhiteSpace(locationInfo.State) &&
!string.IsNullOrWhiteSpace(locationInfo.Zip) &&
!string.IsNullOrWhiteSpace(locationInfo.PhoneNumber) &&
!string.IsNullOrWhiteSpace(locationInfo.LocationName) &&
!string.IsNullOrWhiteSpace(locationInfo.Address) &&
!string.IsNullOrWhiteSpace(locationInfo.Manager);
if (!isLocationInfoValid)
{
args.IsCancelled = true;
await Dialogs.AlertAsync("Please fill out all required fields.", "You cannot proceed");
}
}
#region "Models and Such"
public class CardType
{
[Required]
public string cardChoice { get; set; } = string.Empty;
}
public class CustomizationOptions
{
[Required]
public string custOption { get; set; } = string.Empty;
}
public class MessagingOptions
{
public string verse { get; set; } = string.Empty;
public string signature { get; set; } = string.Empty;
}
public class LogoOptions
{
public string option { get; set; } = string.Empty;
}
public class OfferOptions
{
public string option { get; set; } = string.Empty;
}
public class LocationInfo
{
[Required, Display(Name = "Location Name")]
public string LocationName { get; set; } = string.Empty;
[Required, Display(Name = "Store Manager")]
public string Manager { get; set; } = string.Empty;
[Required]
public string Address { get; set; } = string.Empty;
[Required]
public string City { get; set; } = string.Empty;
[Required]
public string State { get; set; } = string.Empty;
[Required]
public string Zip { get; set; } = string.Empty;
[Required, Display(Name = "Phone Number")]
public string PhoneNumber { get; set; } = string.Empty;
[Required, Display(Name = "Contact Name")]
public string ContactName { get; set; } = string.Empty;
[Required, Display(Name = "Contact Phone")]
public string ContactPhone { get; set; } = string.Empty;
[Required, Display(Name = "Your Email Address")]
public string ContactEmail { get; set; } = string.Empty;
}
public class PaymentInfo
{
public string info { get; set; } = string.Empty;
}
public class AudienceType
{
public string AudienceDescription { get; set; } = string.Empty;
}
#endregion
}

View File

@ -0,0 +1,17 @@
.scrollable-stepper {
border: 1px solid red;
}
.scrollable-stepper .k-stepper {
overflow-y: hidden;
overflow-x: auto;
padding-bottom: 1em;
}
.scrollable-stepper .k-stepper .k-step-list-horizontal {
width: 1600px;
}
.scrollable-stepper .k-stepper .k-progressbar {
width: 1520px; /*(step list width - 1 step width)*/
}

View File

@ -0,0 +1,16 @@
<img alt="" class="img-fluid" src="@ImagePath" />
@code {
public string ImagePath { get; set; } = string.Empty;
protected override Task OnInitializedAsync()//(bool firstRender)
{
//if (firstRender)
{
string[] images = new[] { "d304641b-bfef-4d2f-a715-67184d59b171.jpg", "3db66a22-7e36-4b4a-8d85-3aa27e1c8220.jpg" };
int ix = Random.Shared.Next(0, images.Length);
ImagePath = string.Format("/img/{0}", images[ix]);
}
return base.OnInitializedAsync();
}
}

View File

@ -0,0 +1,63 @@
@page "/weather"
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate a loading indicator
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

10
Components/Routes.razor Normal file
View File

@ -0,0 +1,10 @@
<!-- Wrap the router in a cascading app state -->
<IPAddressGrabber />
<CascadingAppState>
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
</CascadingAppState>

14
Components/_Imports.razor Normal file
View File

@ -0,0 +1,14 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using SummerBestWebForm2
@using SummerBestWebForm2.Components
@using SummerBestWebForm2.AppState
@using Telerik.Blazor
@using Telerik.Blazor.Components
@using Telerik.SvgIcons

67
Program.cs Normal file
View File

@ -0,0 +1,67 @@
using Microsoft.AspNetCore.ResponseCompression;
using SummerBestWebForm2.ClassObj;
using SummerBestWebForm2.Components;
using System.IO.Compression;
namespace SummerBestWebForm2
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.SmallestSize;
});
builder.Services.AddDataProtection();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<HttpContextAccessor>();
builder.Services.AddScoped<IPAddressService>(); // IPADDR
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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();
}
var webSocketOptions = new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(15)
};
app.UseWebSockets(webSocketOptions);
app.UseResponseCompression();
app.UseResponseCaching();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
}
}
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:62562",
"sslPort": 44306
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5052",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7078;http://localhost:5052",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,12 @@
namespace SummerBestWebForm2.SessionState;
public class MdlSession : Dictionary<string, string>
{
/// <summary>
/// Gets or sets the Session Id value.
/// </summary>
public string SessionId { get; set; } = string.Empty;
public string IPAddress { get; set; } = string.Empty;
public bool IsCheckedOut { get; set; } = false;
public DateTimeOffset dtExpires { get; set; } = DateTimeOffset.Now.AddHours(36);
}

View File

@ -0,0 +1,57 @@
namespace SummerBestWebForm2.SessionState;
// INTERFACE
public interface ISessionIdManager
{
Task<string?> GetSessionIdAsync();
Task<string?> GetIPAddressAsync();
}
// CODE
public class SessionIdManager(IHttpContextAccessor httpContextAccessor) : ISessionIdManager
{
private readonly IHttpContextAccessor HttpContextAccessor = httpContextAccessor;
public Task<string?> GetSessionIdAsync()
{
var httpContext = HttpContextAccessor.HttpContext;
string? result;
if (httpContext != null)
{
if (httpContext.Request.Cookies.ContainsKey("sessionId"))
{
result = httpContext.Request.Cookies["sessionId"];
}
else
{
result = Guid.NewGuid().ToString();
httpContext.Response.Cookies.Append("sessionId", result);
}
}
else
{
throw new InvalidOperationException("No HttpContext available");
}
return Task.FromResult(result);
}
public Task<string?> GetIPAddressAsync()
{
var httpContext = HttpContextAccessor.HttpContext;
string? result;
if (httpContext != null)
{
result = httpContext.Connection.RemoteIpAddress?.ToString();// ?? "Not Set";
}
else
{
throw new InvalidOperationException("No HttpContext available");
}
return Task.FromResult(result);
}
}

View File

@ -0,0 +1,132 @@
using System.Net;
namespace SummerBestWebForm2.SessionState;
// INTERFACE
public interface ISessionManager
{
Task<MdlSession> GetSession();
Task UpdateSession(MdlSession session);
}
// CODE
/// <summary>
/// Dictionary containing per-user session objects, keyed
/// by sessionId.
/// </summary>
public class SessionManager : ISessionManager
{
private Dictionary<Guid, MdlSession> _sessions = new Dictionary<Guid, MdlSession>();
private readonly ISessionIdManager _sessionIdManager;
private object syncLock1 = new();
public SessionManager(ISessionIdManager sessionIdManager)
{
_sessionIdManager = sessionIdManager;
}
public async Task<MdlSession> GetSession()
{
string key = await _sessionIdManager.GetSessionIdAsync() ?? "";
string ipAddress = await _sessionIdManager.GetIPAddressAsync() ?? "";
Guid theSessionId;
if (!Guid.TryParse(key, out theSessionId))
theSessionId = Guid.Empty;//Guid.Parse(key);
MdlSession session;
bool needToCreateNew = false;
if (!_sessions.ContainsKey(theSessionId))
{
needToCreateNew = true;
}
else if (_sessions[theSessionId].dtExpires < DateTimeOffset.Now)
needToCreateNew = true;
if (needToCreateNew)
{
session = new MdlSession() { SessionId = key, IPAddress = ipAddress };
_sessions.Add(theSessionId, new MdlSession());
}
else
session = _sessions[theSessionId];
// ensure session isn't checked out by wasm
//while (session.IsCheckedOut)
// await Task.Delay(5);
var endTime = DateTime.UtcNow.AddSeconds(10);
while (session.IsCheckedOut)
{
if (DateTime.UtcNow > endTime)
throw new TimeoutException();
await Task.Delay(5);
}
return session;
}
public async Task UpdateSession(MdlSession session)
{
if (session != null)
{
string key = await _sessionIdManager.GetSessionIdAsync() ?? "";
string ipAddress = await _sessionIdManager.GetIPAddressAsync() ?? "";
Guid theSessionId;
if (!Guid.TryParse(key, out theSessionId))
theSessionId = Guid.Empty;//Guid.Parse(key);
if (_sessions.ContainsKey(theSessionId))
{
//session = new MdlSession() { SessionId = key, IPAddress = ipAddress };
session.SessionId = key;
session.dtExpires = DateTimeOffset.Now.AddHours(36); // Rolling expiration date
_sessions[theSessionId] = session;
_sessions[theSessionId].IsCheckedOut = false;
}
else
{
var sess = new MdlSession() { SessionId = key, IPAddress = ipAddress };
_sessions[theSessionId] = session;
}
// Remove stale sessions
lock (syncLock1)
{
try
{
var lstRemove = new HashSet<Guid>();
foreach (var kvp in _sessions)
{
if (kvp.Value.dtExpires < DateTimeOffset.Now && !lstRemove.Contains(kvp.Key))
lstRemove.Add(kvp.Key);
}
foreach (var rmv in lstRemove)
_sessions.Remove(rmv);
lstRemove.Clear();
}
catch (Exception ex)
{
// do something
}
}
}
} //UpdateSession
/// <summary>
/// Replace the contents of oldSession with the items
/// in newSession.
/// </summary>
/// <param name="newSession"></param>
/// <param name="oldSession"></param>
private void Replace(MdlSession newSession, MdlSession oldSession)
{
oldSession.Clear();
foreach (var key in newSession.Keys)
oldSession.Add(key, newSession[key]);
}
}

19
SummerBestWebForm2.csproj Normal file
View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<Target Name="CustomActionsAfterPublish" AfterTargets="AfterPublish">
<Exec Command="c:\misc\WaitForFolder $(PublishUrl) /nobeep" />
</Target>
<ItemGroup>
<PackageReference Include="kmCommonLibsCore" Version="2.0.0.129" />
<PackageReference Include="Telerik.SvgIcons" Version="4.2.0" />
<PackageReference Include="Telerik.UI.for.Blazor" Version="8.1.1" />
</ItemGroup>
</Project>

25
SummerBestWebForm2.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11415.280 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SummerBestWebForm2", "SummerBestWebForm2.csproj", "{10E16044-8880-42A4-866B-B0461C450A71}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{10E16044-8880-42A4-866B-B0461C450A71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{10E16044-8880-42A4-866B-B0461C450A71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{10E16044-8880-42A4-866B-B0461C450A71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{10E16044-8880-42A4-866B-B0461C450A71}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {53B11938-C281-423B-8D9A-10AA81987064}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

60
wwwroot/app.css Normal file
View File

@ -0,0 +1,60 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e51680;
}
.validation-message {
color: #e51680;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.sbWizard {
text-align: center;
}
.vertical-buttons {
flex-flow: row nowrap;
}
/* set block display */
.block-buttons {
display: flex;
}

BIN
wwwroot/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

BIN
wwwroot/img/A1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
wwwroot/img/A2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
wwwroot/img/A3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
wwwroot/img/A4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
wwwroot/img/A5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
wwwroot/img/A6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
wwwroot/img/B1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/img/B10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
wwwroot/img/B11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
wwwroot/img/B12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
wwwroot/img/B2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
wwwroot/img/B3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
wwwroot/img/B4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
wwwroot/img/B5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/img/B6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
wwwroot/img/B7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/img/B8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
wwwroot/img/B9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
wwwroot/img/RufusHappy.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
wwwroot/img/RufusSad.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
wwwroot/img/favico-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
wwwroot/img/favico-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
wwwroot/img/favico-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
wwwroot/img/favico-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
wwwroot/img/favico-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
wwwroot/img/goodyear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
wwwroot/img/michelin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB