diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..b0e38ab --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file diff --git a/AppState/CascadingAppState.razor b/AppState/CascadingAppState.razor new file mode 100644 index 0000000..e28de8a --- /dev/null +++ b/AppState/CascadingAppState.razor @@ -0,0 +1,7 @@ +@* @using static Microsoft.AspNetCore.Components.Web.RenderMode +@rendermode InteractiveServer +*@ + + @ChildContent + + diff --git a/AppState/CascadingAppState.razor.cs b/AppState/CascadingAppState.razor.cs new file mode 100644 index 0000000..ff096cc --- /dev/null +++ b/AppState/CascadingAppState.razor.cs @@ -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(StorageKey); + var state = JsonSerializer.Deserialize(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 +} diff --git a/AppState/IAppState.cs b/AppState/IAppState.cs new file mode 100644 index 0000000..7f5ef79 --- /dev/null +++ b/AppState/IAppState.cs @@ -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; } +} diff --git a/AppState/MdlAppState.cs b/AppState/MdlAppState.cs new file mode 100644 index 0000000..eaf0c20 --- /dev/null +++ b/AppState/MdlAppState.cs @@ -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; } +} diff --git a/ClassObj/IPAddressService.cs b/ClassObj/IPAddressService.cs new file mode 100644 index 0000000..75d1b7f --- /dev/null +++ b/ClassObj/IPAddressService.cs @@ -0,0 +1,8 @@ +namespace SummerBestWebForm2.ClassObj; + +public class IPAddressService +{ + public const string TokenName = "IPAddress"; + + public string RemoteIpAddress { get; set; } = "Not Set"; +} diff --git a/ClassObj/MdlSessionInfo.cs b/ClassObj/MdlSessionInfo.cs new file mode 100644 index 0000000..0d5e611 --- /dev/null +++ b/ClassObj/MdlSessionInfo.cs @@ -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 + } + +} diff --git a/Components/App.razor b/Components/App.razor new file mode 100644 index 0000000..d83f0ea --- /dev/null +++ b/Components/App.razor @@ -0,0 +1,123 @@ +@implements IDisposable + + + + + + + + + + + + + + + + + + + + + + + + + + + @* *@ + + + + + + + + @* *@ + + + + + +
@errMsg
+@* @if (appState != null) + { +
+      SessionId: @appState.SessionId
+      DateCreated: @appState.DateCreated
+      DateExpires: @appState.DateExpires
+      IPAddress: @appState.myIpAddress
+    
+ } *@ + + + +@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(ClassObj.IPAddressService.TokenName, this.RemoteIpAddress); + return Task.CompletedTask; + } + + void IDisposable.Dispose() + { + _persistingSubscription?.Dispose(); + } +#endregion + + private string errMsg = string.Empty; +} diff --git a/Components/IPAddressGrabber.razor b/Components/IPAddressGrabber.razor new file mode 100644 index 0000000..4b055ab --- /dev/null +++ b/Components/IPAddressGrabber.razor @@ -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(IPAddressService.TokenName, out var address)) + { + this.RemoteIpAddress = address ?? "Not Set"; + this.IPAddressService.RemoteIpAddress = this.RemoteIpAddress; + } + + _subsequentRender = true; + return Task.CompletedTask; + } +} diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..c0aa00b --- /dev/null +++ b/Components/Layout/MainLayout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + + + @Body + diff --git a/Components/Layout/MainLayout.razor.css b/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..038baf1 --- /dev/null +++ b/Components/Layout/MainLayout.razor.css @@ -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; + } diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor new file mode 100644 index 0000000..a4777c9 --- /dev/null +++ b/Components/Layout/NavMenu.razor @@ -0,0 +1,30 @@ + + + + + + diff --git a/Components/Layout/NavMenu.razor.css b/Components/Layout/NavMenu.razor.css new file mode 100644 index 0000000..4e15395 --- /dev/null +++ b/Components/Layout/NavMenu.razor.css @@ -0,0 +1,105 @@ +.navbar-toggler { + appearance: none; + cursor: pointer; + width: 3.5rem; + height: 2.5rem; + color: white; + position: absolute; + top: 0.5rem; + right: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); +} + +.navbar-toggler:checked { + background-color: rgba(255, 255, 255, 0.5); +} + +.top-row { + height: 3.5rem; + background-color: rgba(0,0,0,0.4); +} + +.navbar-brand { + font-size: 1.1rem; +} + +.bi { + display: inline-block; + position: relative; + width: 1.25rem; + height: 1.25rem; + margin-right: 0.75rem; + top: -1px; + background-size: cover; +} + +.bi-house-door-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); +} + +.bi-plus-square-fill-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); +} + +.bi-list-nested-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +} + +.nav-item { + font-size: 0.9rem; + padding-bottom: 0.5rem; +} + + .nav-item:first-of-type { + padding-top: 1rem; + } + + .nav-item:last-of-type { + padding-bottom: 1rem; + } + + .nav-item ::deep .nav-link { + color: #d7d7d7; + background: none; + border: none; + border-radius: 4px; + height: 3rem; + display: flex; + align-items: center; + line-height: 3rem; + width: 100%; + } + +.nav-item ::deep a.active { + background-color: rgba(255,255,255,0.37); + color: white; +} + +.nav-item ::deep .nav-link:hover { + background-color: rgba(255,255,255,0.1); + color: white; +} + +.nav-scrollable { + display: none; +} + +.navbar-toggler:checked ~ .nav-scrollable { + display: block; +} + +@media (min-width: 641px) { + .navbar-toggler { + display: none; + } + + .nav-scrollable { + /* Never collapse the sidebar for wide screens */ + display: block; + + /* Allow sidebar to scroll for tall menus */ + height: calc(100vh - 3.5rem); + overflow-y: auto; + } +} diff --git a/Components/Pages/Counter.razor b/Components/Pages/Counter.razor new file mode 100644 index 0000000..ef23cb3 --- /dev/null +++ b/Components/Pages/Counter.razor @@ -0,0 +1,18 @@ +@page "/counter" + +Counter + +

Counter

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } +} diff --git a/Components/Pages/Error.razor b/Components/Pages/Error.razor new file mode 100644 index 0000000..7d98bd9 --- /dev/null +++ b/Components/Pages/Error.razor @@ -0,0 +1,36 @@ +@page "/Error" +@using System.Diagnostics + +Error + +

Error.

+

An error occurred while processing your request.

+ +@if (ShowRequestId) +{ +

+ Request ID: @RequestId +

+} + +

Development Mode

+

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

+

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

+ +@code { + [CascadingParameter] + private HttpContext? HttpContext { get; set; } + + private string? RequestId { get; set; } + private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + + protected override void OnInitialized() => + RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; +} diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor new file mode 100644 index 0000000..9ac9bf2 --- /dev/null +++ b/Components/Pages/Home.razor @@ -0,0 +1,546 @@ +@rendermode InteractiveServer +@inject PersistentComponentState ApplicationState +@inject ClassObj.IPAddressService IpAddressService +@page "/" + +Welcome + +@if (ProgramClosed) +{ +
+
+
+

Thank you for your interest!

+

Unfortunately, the enrollment period for our Summer Growth Campaign has drawn to a close.

+

Stay tuned for more great campaigns in the future!

+

+
+
+ Thank You! +
+
+
+} +else if (ShowWizard) +{ +
+
+
+ + + + + + + + + + + + @* Need this here to avoid addition of random submit button *@ + + + + + @* *@ + @* Step 1 - plastic card vs postcard *@ + @* *@ + + + + + + + + + + @* Need this here to avoid addition of random submit button *@ + + + + + @* *@ + @* Step 2 - selecting card design *@ + @* *@ + + + + + + + + + + + + + + + + @* *@ + @* Step 3 - For postcards, choose a verse and a signature *@ + @* *@ + + + + + + + + + + + + + @* *@ + @* Step 4 - Logo Selection *@ + @* *@ + + + + + + + + + + + + + + @* *@ + @* Step 5 - Offer selection for Plastic cards *@ + @* *@ + + + + + + + + + + + + + @* *@ + @* Step 6 - Location information *@ + @* *@ + + +
+
+
+ + + +
+
+
+
+
+ @* *@ + @* Step 7 - Payment *@ + @* *@ + + + + + + + + + + + + +
+
+
+
+
+} +else +{ +
+
+
+ Thank you, our customer care team will be in touch with you soon! +
+
+ +
+
+
+ +} + +@* @if (appState != null) +{ +
+    SessionId: @appState.SessionId
+    DateCreated: @appState.DateCreated
+    DateExpires: @appState.DateExpires
+    IPAddress: @appState.myIpAddress
+    IPAddress2: @(IpAddressService.RemoteIpAddress.ToString())
+  
+} + *@ \ No newline at end of file diff --git a/Components/Pages/Home.razor.cs b/Components/Pages/Home.razor.cs new file mode 100644 index 0000000..e4bf074 --- /dev/null +++ b/Components/Pages/Home.razor.cs @@ -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:
{0}
{1}", locationInfo.LocationName, locationInfo.PhoneNumber); + else if (sigTwo) + sigChoice = string.Format("Option E - Name: {0}
Title: {1}
Phone: {2}", customName, customTitle, customPhone); + else if (sigThree) + sigChoice = string.Format("Option F - {0}", customSignature); + + // combine these into customizationInfo + customizationInfo = string.Format("Verse:{0}Signature:{1}", verseChoice, sigChoice); + } + else // isPlasticCard + { + var smallOfferList = new List(); + var bigOfferList = new List(); + + 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("BIG Offers:{0}SMALL Offers:{1}", + string.Join(", ", bigOfferList), string.Join(", ", smallOfferList)); + } + + + + // Logo info + var logos = new List(); + 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("Audience:{0}", string.IsNullOrWhiteSpace(audienceType) ? "NOTHING!" : audienceType); + var locInfoString = string.Format("Location Name:{0}" + + "Manager:{1}" + + "Address:{2}" + + "City:{3}" + + "State:{4}" + + "Zip:{5}" + + "Phone Number:{6}" + + "Contact Name:{7}" + + "Contact Phone:{8}" + + "Contact Email:{9}", + locationInfo.LocationName, locationInfo.Manager, locationInfo.Address, locationInfo.City, locationInfo.State, + locationInfo.Zip, locationInfo.PhoneNumber, locationInfo.ContactName, + locationInfo.ContactPhone, locationInfo.ContactEmail); + + var cardInfoString = string.Format("Card type:{0}, Design {1}" + + "Customization Options:" + + "{2}" + + "Logos:{3}", + cardType, cardDesign, customizationInfo, string.Join(", ", logos)); + + if (int.TryParse(requestedQuantity, out _)) + requestedQuantity = string.Format("{0:#,##0}", int.Parse(requestedQuantity)); + + var paymentString = string.Format("Payment Method:{0}" + + "Requested Quantity:{1}" + + "Additional Comments:{2}", paymentMethod, requestedQuantity, additionalComments); + var ipString = string.Format("IP Address{0}", + string.IsNullOrWhiteSpace(IpAddressService.RemoteIpAddress.ToString()) ? "NONE" : IpAddressService.RemoteIpAddress.ToString()); + + em.HtmlBody = "
You have a new enrollment:

" + + targetAudience + locInfoString + cardInfoString + paymentString + ipString + "
"; + + 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 audiences { get; set; } = new List + { + 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 +} diff --git a/Components/Pages/Home.razor.css b/Components/Pages/Home.razor.css new file mode 100644 index 0000000..1fdcf49 --- /dev/null +++ b/Components/Pages/Home.razor.css @@ -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)*/ + } diff --git a/Components/Pages/RandomImage.razor b/Components/Pages/RandomImage.razor new file mode 100644 index 0000000..6028b0a --- /dev/null +++ b/Components/Pages/RandomImage.razor @@ -0,0 +1,16 @@ + +@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(); + } +} diff --git a/Components/Pages/Weather.razor b/Components/Pages/Weather.razor new file mode 100644 index 0000000..cef893e --- /dev/null +++ b/Components/Pages/Weather.razor @@ -0,0 +1,63 @@ +@page "/weather" + +Weather + +

Weather

+ +

This component demonstrates showing data.

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

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + private WeatherForecast[]? forecasts; + + protected override async Task OnInitializedAsync() + { + // Simulate asynchronous loading to demonstrate 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); + } +} diff --git a/Components/Routes.razor b/Components/Routes.razor new file mode 100644 index 0000000..09ab959 --- /dev/null +++ b/Components/Routes.razor @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Components/_Imports.razor b/Components/_Imports.razor new file mode 100644 index 0000000..c999871 --- /dev/null +++ b/Components/_Imports.razor @@ -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 diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..0495bd4 --- /dev/null +++ b/Program.cs @@ -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(); + options.Providers.Add(); + }); + builder.Services.Configure(options => + { + options.Level = CompressionLevel.Fastest; + }); + builder.Services.Configure(options => + { + options.Level = CompressionLevel.SmallestSize; + }); + builder.Services.AddDataProtection(); + + builder.Services.AddHttpContextAccessor(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); // 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() + .AddInteractiveServerRenderMode(); + + app.Run(); + } + } +} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..e19076b --- /dev/null +++ b/Properties/launchSettings.json @@ -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" + } + } + } + } diff --git a/SessionState/MdlSession.cs b/SessionState/MdlSession.cs new file mode 100644 index 0000000..7e5861d --- /dev/null +++ b/SessionState/MdlSession.cs @@ -0,0 +1,12 @@ +namespace SummerBestWebForm2.SessionState; + +public class MdlSession : Dictionary +{ + /// + /// Gets or sets the Session Id value. + /// + 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); +} diff --git a/SessionState/SessionIdManager.cs b/SessionState/SessionIdManager.cs new file mode 100644 index 0000000..7ac22cf --- /dev/null +++ b/SessionState/SessionIdManager.cs @@ -0,0 +1,57 @@ +namespace SummerBestWebForm2.SessionState; + +// INTERFACE + +public interface ISessionIdManager +{ + Task GetSessionIdAsync(); + Task GetIPAddressAsync(); +} + +// CODE + +public class SessionIdManager(IHttpContextAccessor httpContextAccessor) : ISessionIdManager +{ + private readonly IHttpContextAccessor HttpContextAccessor = httpContextAccessor; + + public Task 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 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); + } + +} diff --git a/SessionState/SessionManager.cs b/SessionState/SessionManager.cs new file mode 100644 index 0000000..f69fb38 --- /dev/null +++ b/SessionState/SessionManager.cs @@ -0,0 +1,132 @@ +using System.Net; + +namespace SummerBestWebForm2.SessionState; + +// INTERFACE + +public interface ISessionManager +{ + Task GetSession(); + Task UpdateSession(MdlSession session); +} + +// CODE + +/// +/// Dictionary containing per-user session objects, keyed +/// by sessionId. +/// +public class SessionManager : ISessionManager +{ + private Dictionary _sessions = new Dictionary(); + private readonly ISessionIdManager _sessionIdManager; + private object syncLock1 = new(); + + public SessionManager(ISessionIdManager sessionIdManager) + { + _sessionIdManager = sessionIdManager; + } + + public async Task 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(); + 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 + + + /// + /// Replace the contents of oldSession with the items + /// in newSession. + /// + /// + /// + private void Replace(MdlSession newSession, MdlSession oldSession) + { + oldSession.Clear(); + foreach (var key in newSession.Keys) + oldSession.Add(key, newSession[key]); + } +} diff --git a/SummerBestWebForm2.csproj b/SummerBestWebForm2.csproj new file mode 100644 index 0000000..ae51751 --- /dev/null +++ b/SummerBestWebForm2.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/SummerBestWebForm2.sln b/SummerBestWebForm2.sln new file mode 100644 index 0000000..77c57fb --- /dev/null +++ b/SummerBestWebForm2.sln @@ -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 diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/wwwroot/app.css b/wwwroot/app.css new file mode 100644 index 0000000..2240128 --- /dev/null +++ b/wwwroot/app.css @@ -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; +} diff --git a/wwwroot/favicon.png b/wwwroot/favicon.png new file mode 100644 index 0000000..8422b59 Binary files /dev/null and b/wwwroot/favicon.png differ diff --git a/wwwroot/img/1181c4aa-4f3f-4eb4-8d5d-7eff0131d5f2.png b/wwwroot/img/1181c4aa-4f3f-4eb4-8d5d-7eff0131d5f2.png new file mode 100644 index 0000000..506b20d Binary files /dev/null and b/wwwroot/img/1181c4aa-4f3f-4eb4-8d5d-7eff0131d5f2.png differ diff --git a/wwwroot/img/34d1685e-d655-421b-aef8-e077140534f9.png b/wwwroot/img/34d1685e-d655-421b-aef8-e077140534f9.png new file mode 100644 index 0000000..a6b4c7b Binary files /dev/null and b/wwwroot/img/34d1685e-d655-421b-aef8-e077140534f9.png differ diff --git a/wwwroot/img/3db66a22-7e36-4b4a-8d85-3aa27e1c8220.jpg b/wwwroot/img/3db66a22-7e36-4b4a-8d85-3aa27e1c8220.jpg new file mode 100644 index 0000000..d1a0b57 Binary files /dev/null and b/wwwroot/img/3db66a22-7e36-4b4a-8d85-3aa27e1c8220.jpg differ diff --git a/wwwroot/img/6784cef1-9ed9-4c91-94bc-5e7a267b5a4f.png b/wwwroot/img/6784cef1-9ed9-4c91-94bc-5e7a267b5a4f.png new file mode 100644 index 0000000..f31db62 Binary files /dev/null and b/wwwroot/img/6784cef1-9ed9-4c91-94bc-5e7a267b5a4f.png differ diff --git a/wwwroot/img/7ccdfcc7-f91b-497e-8c19-ebb3a4021ea3.png b/wwwroot/img/7ccdfcc7-f91b-497e-8c19-ebb3a4021ea3.png new file mode 100644 index 0000000..bcbfa65 Binary files /dev/null and b/wwwroot/img/7ccdfcc7-f91b-497e-8c19-ebb3a4021ea3.png differ diff --git a/wwwroot/img/A1.png b/wwwroot/img/A1.png new file mode 100644 index 0000000..17faf59 Binary files /dev/null and b/wwwroot/img/A1.png differ diff --git a/wwwroot/img/A2.png b/wwwroot/img/A2.png new file mode 100644 index 0000000..14d9a56 Binary files /dev/null and b/wwwroot/img/A2.png differ diff --git a/wwwroot/img/A3.png b/wwwroot/img/A3.png new file mode 100644 index 0000000..bb2acf7 Binary files /dev/null and b/wwwroot/img/A3.png differ diff --git a/wwwroot/img/A4.png b/wwwroot/img/A4.png new file mode 100644 index 0000000..34481b9 Binary files /dev/null and b/wwwroot/img/A4.png differ diff --git a/wwwroot/img/A5.png b/wwwroot/img/A5.png new file mode 100644 index 0000000..c5417e1 Binary files /dev/null and b/wwwroot/img/A5.png differ diff --git a/wwwroot/img/A6.png b/wwwroot/img/A6.png new file mode 100644 index 0000000..5c3ccb7 Binary files /dev/null and b/wwwroot/img/A6.png differ diff --git a/wwwroot/img/B1.png b/wwwroot/img/B1.png new file mode 100644 index 0000000..d406fd8 Binary files /dev/null and b/wwwroot/img/B1.png differ diff --git a/wwwroot/img/B10.png b/wwwroot/img/B10.png new file mode 100644 index 0000000..9da340a Binary files /dev/null and b/wwwroot/img/B10.png differ diff --git a/wwwroot/img/B11.png b/wwwroot/img/B11.png new file mode 100644 index 0000000..86ebfae Binary files /dev/null and b/wwwroot/img/B11.png differ diff --git a/wwwroot/img/B12.png b/wwwroot/img/B12.png new file mode 100644 index 0000000..f66658f Binary files /dev/null and b/wwwroot/img/B12.png differ diff --git a/wwwroot/img/B2.png b/wwwroot/img/B2.png new file mode 100644 index 0000000..e9afd5f Binary files /dev/null and b/wwwroot/img/B2.png differ diff --git a/wwwroot/img/B3.png b/wwwroot/img/B3.png new file mode 100644 index 0000000..ae3b076 Binary files /dev/null and b/wwwroot/img/B3.png differ diff --git a/wwwroot/img/B4.png b/wwwroot/img/B4.png new file mode 100644 index 0000000..1f7d993 Binary files /dev/null and b/wwwroot/img/B4.png differ diff --git a/wwwroot/img/B5.png b/wwwroot/img/B5.png new file mode 100644 index 0000000..c3d3bd4 Binary files /dev/null and b/wwwroot/img/B5.png differ diff --git a/wwwroot/img/B6.png b/wwwroot/img/B6.png new file mode 100644 index 0000000..321e0d1 Binary files /dev/null and b/wwwroot/img/B6.png differ diff --git a/wwwroot/img/B7.png b/wwwroot/img/B7.png new file mode 100644 index 0000000..c0ddf6e Binary files /dev/null and b/wwwroot/img/B7.png differ diff --git a/wwwroot/img/B8.png b/wwwroot/img/B8.png new file mode 100644 index 0000000..e49be7a Binary files /dev/null and b/wwwroot/img/B8.png differ diff --git a/wwwroot/img/B9.png b/wwwroot/img/B9.png new file mode 100644 index 0000000..a58b1a6 Binary files /dev/null and b/wwwroot/img/B9.png differ diff --git a/wwwroot/img/Plastic1_Back.png b/wwwroot/img/Plastic1_Back.png new file mode 100644 index 0000000..b839cdb Binary files /dev/null and b/wwwroot/img/Plastic1_Back.png differ diff --git a/wwwroot/img/Plastic1_Front.png b/wwwroot/img/Plastic1_Front.png new file mode 100644 index 0000000..2602b33 Binary files /dev/null and b/wwwroot/img/Plastic1_Front.png differ diff --git a/wwwroot/img/Plastic2_Back.png b/wwwroot/img/Plastic2_Back.png new file mode 100644 index 0000000..8c8d75f Binary files /dev/null and b/wwwroot/img/Plastic2_Back.png differ diff --git a/wwwroot/img/Plastic2_Front.png b/wwwroot/img/Plastic2_Front.png new file mode 100644 index 0000000..a7d95aa Binary files /dev/null and b/wwwroot/img/Plastic2_Front.png differ diff --git a/wwwroot/img/Plastic3_Back.png b/wwwroot/img/Plastic3_Back.png new file mode 100644 index 0000000..5c67dd9 Binary files /dev/null and b/wwwroot/img/Plastic3_Back.png differ diff --git a/wwwroot/img/Plastic3_Front.png b/wwwroot/img/Plastic3_Front.png new file mode 100644 index 0000000..5b7c4fc Binary files /dev/null and b/wwwroot/img/Plastic3_Front.png differ diff --git a/wwwroot/img/Postcard1_Back.png b/wwwroot/img/Postcard1_Back.png new file mode 100644 index 0000000..32e0483 Binary files /dev/null and b/wwwroot/img/Postcard1_Back.png differ diff --git a/wwwroot/img/Postcard1_Front.png b/wwwroot/img/Postcard1_Front.png new file mode 100644 index 0000000..9e1797f Binary files /dev/null and b/wwwroot/img/Postcard1_Front.png differ diff --git a/wwwroot/img/Postcard2_Back.png b/wwwroot/img/Postcard2_Back.png new file mode 100644 index 0000000..d06847e Binary files /dev/null and b/wwwroot/img/Postcard2_Back.png differ diff --git a/wwwroot/img/Postcard2_Front.png b/wwwroot/img/Postcard2_Front.png new file mode 100644 index 0000000..7e5b7a3 Binary files /dev/null and b/wwwroot/img/Postcard2_Front.png differ diff --git a/wwwroot/img/Postcard3_Back.png b/wwwroot/img/Postcard3_Back.png new file mode 100644 index 0000000..72082d5 Binary files /dev/null and b/wwwroot/img/Postcard3_Back.png differ diff --git a/wwwroot/img/Postcard3_Front.png b/wwwroot/img/Postcard3_Front.png new file mode 100644 index 0000000..06e2473 Binary files /dev/null and b/wwwroot/img/Postcard3_Front.png differ diff --git a/wwwroot/img/RufusHappy.jpg b/wwwroot/img/RufusHappy.jpg new file mode 100644 index 0000000..f3dab9f Binary files /dev/null and b/wwwroot/img/RufusHappy.jpg differ diff --git a/wwwroot/img/RufusSad.jpg b/wwwroot/img/RufusSad.jpg new file mode 100644 index 0000000..657c2a2 Binary files /dev/null and b/wwwroot/img/RufusSad.jpg differ diff --git a/wwwroot/img/a41f648d-6b39-41f1-bbe5-084fb8a71a30.png b/wwwroot/img/a41f648d-6b39-41f1-bbe5-084fb8a71a30.png new file mode 100644 index 0000000..eaf0c2b Binary files /dev/null and b/wwwroot/img/a41f648d-6b39-41f1-bbe5-084fb8a71a30.png differ diff --git a/wwwroot/img/ad5a2b69-a493-429b-9edd-8320ec1e9c33.png b/wwwroot/img/ad5a2b69-a493-429b-9edd-8320ec1e9c33.png new file mode 100644 index 0000000..f0eb69b Binary files /dev/null and b/wwwroot/img/ad5a2b69-a493-429b-9edd-8320ec1e9c33.png differ diff --git a/wwwroot/img/cfeb51c5-5373-44b5-be19-dadea452cc41.png b/wwwroot/img/cfeb51c5-5373-44b5-be19-dadea452cc41.png new file mode 100644 index 0000000..06e4c18 Binary files /dev/null and b/wwwroot/img/cfeb51c5-5373-44b5-be19-dadea452cc41.png differ diff --git a/wwwroot/img/d304641b-bfef-4d2f-a715-67184d59b171.jpg b/wwwroot/img/d304641b-bfef-4d2f-a715-67184d59b171.jpg new file mode 100644 index 0000000..cc6f1fc Binary files /dev/null and b/wwwroot/img/d304641b-bfef-4d2f-a715-67184d59b171.jpg differ diff --git a/wwwroot/img/favico-128.png b/wwwroot/img/favico-128.png new file mode 100644 index 0000000..8a9a8f2 Binary files /dev/null and b/wwwroot/img/favico-128.png differ diff --git a/wwwroot/img/favico-256.png b/wwwroot/img/favico-256.png new file mode 100644 index 0000000..62a224a Binary files /dev/null and b/wwwroot/img/favico-256.png differ diff --git a/wwwroot/img/favico-32.png b/wwwroot/img/favico-32.png new file mode 100644 index 0000000..8d16f07 Binary files /dev/null and b/wwwroot/img/favico-32.png differ diff --git a/wwwroot/img/favico-512.png b/wwwroot/img/favico-512.png new file mode 100644 index 0000000..a671f7d Binary files /dev/null and b/wwwroot/img/favico-512.png differ diff --git a/wwwroot/img/favico-64.png b/wwwroot/img/favico-64.png new file mode 100644 index 0000000..ee769f2 Binary files /dev/null and b/wwwroot/img/favico-64.png differ diff --git a/wwwroot/img/goodyear.png b/wwwroot/img/goodyear.png new file mode 100644 index 0000000..00727d4 Binary files /dev/null and b/wwwroot/img/goodyear.png differ diff --git a/wwwroot/img/michelin.png b/wwwroot/img/michelin.png new file mode 100644 index 0000000..97f4023 Binary files /dev/null and b/wwwroot/img/michelin.png differ diff --git a/wwwroot/img/t-r-photography-TzjMd7i5WQI-unsplash.jpg b/wwwroot/img/t-r-photography-TzjMd7i5WQI-unsplash.jpg new file mode 100644 index 0000000..aed8a23 Binary files /dev/null and b/wwwroot/img/t-r-photography-TzjMd7i5WQI-unsplash.jpg differ