commit 001b4fc41da9f08c20497a0ee0463e18ffe0fcc9 Author: Josh Deck Date: Thu Sep 18 12:54:02 2025 -0400 Initial Version for Review 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/.gitattributes b/.gitattributes new file mode 100644 index 0000000..09d1324 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/AppState/CascadingAppState.razor b/AppState/CascadingAppState.razor new file mode 100644 index 0000000..91a5d9a --- /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..a0ed4b9 --- /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 WinterBestCustomerWebForm.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..70fb73d --- /dev/null +++ b/AppState/IAppState.cs @@ -0,0 +1,10 @@ +namespace WinterBestCustomerWebForm.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..5f02f83 --- /dev/null +++ b/AppState/MdlAppState.cs @@ -0,0 +1,10 @@ +namespace WinterBestCustomerWebForm.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..aa5a5e8 --- /dev/null +++ b/ClassObj/IPAddressService.cs @@ -0,0 +1,8 @@ +namespace WinterBestCustomerWebForm.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..11b4298 --- /dev/null +++ b/ClassObj/MdlSessionInfo.cs @@ -0,0 +1,15 @@ +namespace WinterBestCustomerWebForm; + +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..e70b4f0 --- /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..92fa254 --- /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..3b825b2 --- /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..6cf1cfd --- /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..0d5af63 --- /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..7719561 --- /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/Error.razor b/Components/Pages/Error.razor new file mode 100644 index 0000000..ca51e61 --- /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..22974f6 --- /dev/null +++ b/Components/Pages/Home.razor @@ -0,0 +1,464 @@ +@rendermode InteractiveServer +@inject PersistentComponentState ApplicationState +@inject ClassObj.IPAddressService IpAddressService +@page "/" + +Welcome + +@if (ProgramClosed) +{ +
+
+
+

Thank you for your interest!

+

Unfortunately, the enrollment period for our Winter 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 - greeting card vs postcard *@ + @* *@ + + + + + + + + + + @* Need this here to avoid addition of random submit button *@ + + + + + @* *@ + @* Step 2 - selecting card design *@ + @* *@ + + + + + + + + + + + + + + + + @* *@ + @* Step 3 - Greeting, verse, signature *@ + @* *@ + + + + + + + + + + + + + @* *@ + @* Step 4 - Logo Selection *@ + @* *@ + + + + + + + + + + + + + + @* *@ + @* Step 5 - Location information *@ + @* *@ + + +
+
+
+ + + +
+
+
+
+
+ @* *@ + @* Step 6 - 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..99d201d --- /dev/null +++ b/Components/Pages/Home.razor.cs @@ -0,0 +1,386 @@ +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 WinterBestCustomerWebForm.AppState; +using WinterBestCustomerWebForm.ClassObj; + +namespace WinterBestCustomerWebForm.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" : "Greeting Card"; + var cardDesign = string.Empty; + if (designOne) + cardDesign = isGreetingCard ? "A" : "E"; + else if (designTwo) + cardDesign = isGreetingCard ? "B" : "F"; + else if (designThree) + cardDesign = isGreetingCard ? "C" : "G"; + else if (designFour) + cardDesign = isGreetingCard ? "D" : "H"; + else if (designCustom) + cardDesign = "CUSTOM"; + // Customizations + var greetingChoice = string.Empty; + var verseChoice = string.Empty; + var sigChoice = string.Empty; + + if (greetingOne) + greetingChoice = "Happy Holidays"; + else if (greetingTwo) + greetingChoice = "Merry Christmas"; + else if (greetingThree) + greetingChoice = "Season's Greetings"; + + 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 + var customizationInfo = string.Format("Greeting:{0}Verse:{1}Signature:{2}", greetingChoice, verseChoice, sigChoice); + + + + // Logo info + var logos = new List(); + if (logoOnCard) + logos.Add("Logo on Card"); + if (isGreetingCard && logoOnEnvelope) + logos.Add("Logo on Envelope"); + 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 = "WBC Order Form Submission"; + em.AddAddress(enuAddressType.From, "support@keymotive.us", "Winter Program Enrollment"); + //em.AddAddress(enuAddressType.To, "support@keymotive.net", "KeyMotive Support"); + //em.AddAddress(enuAddressType.To, "jondeck@keymotive.net", "Jon Deck"); + //em.AddAddress(enuAddressType.To, "amijat@keymotive.net", "Andrea Mijat"); + em.AddAddress(enuAddressType.To, "joshdeck@keymotive.net", "Josh Deck"); + + 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:

" + + 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; } = true; + 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 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 isGreetingCard { 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 designFour = 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 greetingOne = true; + public bool greetingTwo = false; + public bool greetingThree = false; + 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 logoOnCard = true; + public bool logoOnEnvelope = true; + public bool isLogoValid = true; + + // 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() + { + isGreetingCard = !isGreetingCard; + isPostcard = !isPostcard; + } + + 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..dadb702 --- /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..968b12d --- /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/Routes.razor b/Components/Routes.razor new file mode 100644 index 0000000..2b72e16 --- /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..6389827 --- /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 WinterBestCustomerWebForm +@using WinterBestCustomerWebForm.Components +@using WinterBestCustomerWebForm.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..e5d4731 --- /dev/null +++ b/Program.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.ResponseCompression; +using WinterBestCustomerWebForm.ClassObj; +using WinterBestCustomerWebForm.Components; +using System.IO.Compression; + +namespace WinterBestCustomerWebForm +{ + 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..9ddb344 --- /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..2992e39 --- /dev/null +++ b/SessionState/MdlSession.cs @@ -0,0 +1,12 @@ +namespace WinterBestCustomerWebForm.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..45f44ad --- /dev/null +++ b/SessionState/SessionIdManager.cs @@ -0,0 +1,57 @@ +namespace WinterBestCustomerWebForm.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..1dacd84 --- /dev/null +++ b/SessionState/SessionManager.cs @@ -0,0 +1,132 @@ +using System.Net; + +namespace WinterBestCustomerWebForm.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/WinterBestCustomerWebForm.csproj b/WinterBestCustomerWebForm.csproj new file mode 100644 index 0000000..ae51751 --- /dev/null +++ b/WinterBestCustomerWebForm.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/WinterBestCustomerWebForm.sln b/WinterBestCustomerWebForm.sln new file mode 100644 index 0000000..10cef12 --- /dev/null +++ b/WinterBestCustomerWebForm.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.36105.23 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WinterBestCustomerWebForm", "WinterBestCustomerWebForm.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..a34cd70 --- /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..23160a4 --- /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..8bfd350 --- /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/Greeting1.png b/wwwroot/img/Greeting1.png new file mode 100644 index 0000000..87dd22e Binary files /dev/null and b/wwwroot/img/Greeting1.png differ diff --git a/wwwroot/img/Greeting2.png b/wwwroot/img/Greeting2.png new file mode 100644 index 0000000..9938402 Binary files /dev/null and b/wwwroot/img/Greeting2.png differ diff --git a/wwwroot/img/Greeting3.png b/wwwroot/img/Greeting3.png new file mode 100644 index 0000000..01dca7a Binary files /dev/null and b/wwwroot/img/Greeting3.png differ diff --git a/wwwroot/img/Greeting4.png b/wwwroot/img/Greeting4.png new file mode 100644 index 0000000..b2ee040 Binary files /dev/null and b/wwwroot/img/Greeting4.png differ diff --git a/wwwroot/img/GreetingCards.png b/wwwroot/img/GreetingCards.png new file mode 100644 index 0000000..1d4b62d Binary files /dev/null and b/wwwroot/img/GreetingCards.png differ diff --git a/wwwroot/img/HomeCollage.png b/wwwroot/img/HomeCollage.png new file mode 100644 index 0000000..e1daaca Binary files /dev/null and b/wwwroot/img/HomeCollage.png differ diff --git a/wwwroot/img/Postcard1.jpg b/wwwroot/img/Postcard1.jpg new file mode 100644 index 0000000..945ae26 Binary files /dev/null and b/wwwroot/img/Postcard1.jpg differ diff --git a/wwwroot/img/Postcard2.jpg b/wwwroot/img/Postcard2.jpg new file mode 100644 index 0000000..7612e03 Binary files /dev/null and b/wwwroot/img/Postcard2.jpg differ diff --git a/wwwroot/img/Postcard3.jpg b/wwwroot/img/Postcard3.jpg new file mode 100644 index 0000000..94f9453 Binary files /dev/null and b/wwwroot/img/Postcard3.jpg differ diff --git a/wwwroot/img/Postcard4.jpg b/wwwroot/img/Postcard4.jpg new file mode 100644 index 0000000..399e12c Binary files /dev/null and b/wwwroot/img/Postcard4.jpg differ diff --git a/wwwroot/img/Postcards.png b/wwwroot/img/Postcards.png new file mode 100644 index 0000000..64be306 Binary files /dev/null and b/wwwroot/img/Postcards.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/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/greetings/HappyHolidays.png b/wwwroot/img/greetings/HappyHolidays.png new file mode 100644 index 0000000..4c2d333 Binary files /dev/null and b/wwwroot/img/greetings/HappyHolidays.png differ diff --git a/wwwroot/img/greetings/MerryChristmas.png b/wwwroot/img/greetings/MerryChristmas.png new file mode 100644 index 0000000..2ec9240 Binary files /dev/null and b/wwwroot/img/greetings/MerryChristmas.png differ diff --git a/wwwroot/img/greetings/SeasonsGreetings.png b/wwwroot/img/greetings/SeasonsGreetings.png new file mode 100644 index 0000000..4de76eb Binary files /dev/null and b/wwwroot/img/greetings/SeasonsGreetings.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