bunch o stuff

This commit is contained in:
2026-04-25 13:14:08 -05:00
parent fc7db1fedd
commit a1fddb3513
18 changed files with 1037 additions and 190 deletions

View File

@@ -1,86 +1,108 @@
@page "/book"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Forms
<div class="container booking-page">
<div class="section-title">
<h1>Request an Appointment</h1>
<p>Complete the form below and a technician will review your case.</p>
@if(!Complete)
{
<div class="container booking-page">
<div class="section-title">
<h1>Request an Appointment</h1>
<p>Complete the form below and a technician will review your case.</p>
</div>
<EditForm FormName="RepairRequestForm" Model="Model" OnValidSubmit="HandleSubmit" On class="booking-form-wrapper">
<DataAnnotationsValidator />
<div class="form-grid">
<div class="form-column">
<h3 class="form-heading">1. Appliance Details</h3>
<div class="field-group">
<label>Appliance Type</label>
<InputSelect @bind-Value="Model.Type" class="custom-input">
<option value="">Select an appliance...</option>
<option>Refrigerator</option>
<option>Washing Machine</option>
<option>Dishwasher</option>
<option>Oven / Stove</option>
<option>Dryer</option>
</InputSelect>
<ValidationMessage For="@(() => Model.Type)" class="text-danger" />
</div>
<div class="field-group">
<label>Appliance Brand</label>
<InputSelect @bind-Value="Model.Brand" class="custom-input">
<option value="">Select a brand...</option>
<option>LG</option>
<option>Maytag</option>
<option>Whirlpool</option>
<option>Kitchen Aid</option>
<option>Amana</option>
<option>GE</option>
<option>Samsung</option>
<option>Bosch</option>
<option>Frigidaire</option>
<option>Kenmore</option>
<option>Other (please include in notes)</option>
</InputSelect>
<ValidationMessage For="@(() => Model.Brand)" class="text-danger" />
</div>
<div class="field-group">
<label>Describe the Issue</label>
<InputTextArea @bind-Value="Model.Notes"
placeholder="e.g. Making a clicking noise, not draining, error code F1E2..."
class="custom-input textarea" />
<ValidationMessage For="@(() => Model.Notes)" class="text-danger" />
</div>
</div>
<div class="form-column">
<h3 class="form-heading">2. Photos & Contact</h3>
<div class="upload-zone">
<p><strong>Upload Photos/Video (10MB max)</strong></p>
<p class="small-text">Tip: A photo of the <u>model number sticker</u> helps us arrive with the right parts!</p>
<InputFile OnChange="HandleFiles" multiple class="file-input" id="file-upload" />
<label for="file-upload" class="btn btn-secondary">Add Media</label>
@if (SelectedFiles.Any())
{
<div class="file-count">@SelectedFiles.Count files attached</div>
}
</div>
<div class="field-group">
<label>Full Name</label>
<InputText @bind-Value="Model.Name" class="custom-input" />
<ValidationMessage For="@(() => Model.Name)" class="text-danger" />
</div>
<div class="field-group">
<label>Phone Number</label>
<InputText @bind-Value="Model.Phone" class="custom-input" />
<ValidationMessage For="@(() => Model.Phone)" class="text-danger" />
</div>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-large">Submit Repair Request</button>
</div>
</EditForm>
</div>
<EditForm Model="@Model" OnValidSubmit="HandleSubmit" class="booking-form-wrapper">
<DataAnnotationsValidator />
<div class="form-grid">
<div class="form-column">
<h3 class="form-heading">1. Appliance Details</h3>
<div class="field-group">
<label>Appliance Type</label>
<InputSelect @bind-Value="Model.Type" class="custom-input">
<option value="">Select an appliance...</option>
<option>Refrigerator</option>
<option>Washing Machine</option>
<option>Dishwasher</option>
<option>Oven / Stove</option>
<option>Dryer</option>
</InputSelect>
</div>
<div class="field-group">
<label>Appliance Brand</label>
<InputSelect @bind-Value="Model.Brand" class="custom-input">
<option value="">Select a brand...</option>
<option>LG</option>
<option>Maytag</option>
<option>Whirlpool</option>
<option>Kitchen Aid</option>
<option>Amana</option>
<option>GE</option>
<option>Samsung</option>
<option>Bosch</option>
<option>Frigidaire</option>
<option>Kenmore</option>
<option>Other (please include in notes)</option>
</InputSelect>
</div>
<div class="field-group">
<label>Describe the Issue</label>
<InputTextArea @bind-Value="Model.Notes"
placeholder="e.g. Making a clicking noise, not draining, error code F1E2..."
class="custom-input textarea" />
</div>
</div>
<div class="form-column">
<h3 class="form-heading">2. Photos & Contact</h3>
<div class="upload-zone">
<p><strong>Upload Photos/Video</strong></p>
<p class="small-text">Tip: A photo of the <u>model number sticker</u> helps us arrive with the right parts!</p>
<InputFile OnChange="HandleFiles" multiple class="file-input" id="file-upload" />
<label for="file-upload" class="btn btn-secondary">Add Media</label>
@if (SelectedFiles.Any())
{
<div class="file-count">@SelectedFiles.Count files attached</div>
}
</div>
<div class="field-group">
<label>Full Name</label>
<InputText @bind-Value="Model.Name" class="custom-input" />
</div>
<div class="field-group">
<label>Phone Number</label>
<InputText @bind-Value="Model.Phone" class="custom-input" />
</div>
</div>
}
else
{
<div class="complete-container">
<div class="complete-content">
<h1 class="complete-heading">Thank You!</h1>
<p class="complete-subheading">We will be contacting you shortly.</p>
<p class="complete-subheading">Your request number is: @Model.RequestNumber</p>
<NavLink class="btn-home" href="" Match="NavLinkMatch.All">
<span class="home-icon">🏠</span> Back to Home
</NavLink>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-large">Submit Repair Request</button>
</div>
</EditForm>
</div>
</div>
}

View File

@@ -2,16 +2,70 @@
namespace ApplianceRepair.Components.Pages
{
public partial class Book()
public static class RequestNumberGenerator
{
private RepairRequestModel Model = new();
private List<IBrowserFile> SelectedFiles = new();
private static readonly char[] _chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray();
public static string Generate(int length = 6)
{
var result = new char[length];
// Use Random.Shared in .NET 6+ for thread safety
for (int i = 0; i < length; i++)
{
result[i] = _chars[Random.Shared.Next(_chars.Length)];
}
return new string(result);
}
}
public partial class Book(RepairRequestReader repairRequestReader, RepairRequestMediaReader repairRequestMediaReader)
{
public RepairRequestModel Model = new();
public List<IBrowserFile> SelectedFiles = new();
public bool Complete = false;
private void HandleFiles(InputFileChangeEventArgs e) => SelectedFiles.AddRange(e.GetMultipleFiles());
private async Task HandleSubmit()
{
// Logic to process the request
Model.RequestNumber = RequestNumberGenerator.Generate();
Model.CreatedAt = DateTime.Now;
Model.UpdatedAt = DateTime.Now;
await repairRequestReader.AddRecord(Model);
var imageUploadPath = Path.Combine(Environment.CurrentDirectory, "wwwroot", "uploads");
if (!Directory.Exists(imageUploadPath)) Directory.CreateDirectory(imageUploadPath);
foreach (var file in SelectedFiles)
{
try
{
var trustedFileName = Path.GetRandomFileName() + Path.GetExtension(file.Name);
var path = Path.Combine(imageUploadPath, trustedFileName);
using var stream = file.OpenReadStream(maxAllowedSize: 1024 * 1024 * 10);
using var fileStream = new FileStream(path, FileMode.Create);
await stream.CopyToAsync(fileStream);
var mediaRecord = new RepairRequestMediaRecord()
{
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
RequestNumber = Model.RequestNumber,
MediaPath = path,
};
await repairRequestMediaReader.AddRecord(mediaRecord);
}
catch (Exception ex)
{
// probably need to show this to the user somehow, something prettier tho
Console.WriteLine($"File: {file.Name} Error: {ex.Message}");
}
}
Complete = true;
}
}
}

View File

@@ -93,7 +93,61 @@
transition: all 0.3s ease;
}
/* Match your mobile responsiveness */
.complete-container {
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 20px;
}
.complete-content {
max-width: 800px;
}
.complete-heading {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
}
.complete-subheading {
font-size: 1.2rem;
color: #555;
line-height: 1.5;
}
::deep .btn-home {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s ease, transform 0.1s ease;
border: none;
cursor: pointer;
}
::deep .btn-home:hover {
background-color: #0056b3;
color: white;
text-decoration: none;
}
::deep .btn-home:active {
transform: scale(0.98);
}
.home-icon {
margin-right: 8px;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
@@ -102,4 +156,12 @@
.booking-form-wrapper {
padding: 20px;
}
.complete-heading {
font-size: 1.8rem; /* Slightly smaller for small screens */
}
.complete-subheading {
font-size: 1rem;
}
}

View File

@@ -2,7 +2,7 @@
namespace ApplianceRepair.Components.Pages
{
public partial class Home(IMemoryCache cache, HomePageReader homePageReader, ContentCardReader contentCardReader)
public partial class Home(IMemoryCache cache, HomePageReader homePageReader, ContentCardReader contentCardReader, BusinessConfigReader businessConfigReader)
{
private HomePageModel? Model;
@@ -10,7 +10,12 @@ namespace ApplianceRepair.Components.Pages
{
if (!cache.TryGetValue(nameof(HomePageModel), out Model))
{
Model = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent;
var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
Model = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(24))

View File

@@ -21,7 +21,7 @@ else
<h1>@Model.HeaderLine1 <br><span>@Model.HeaderLine2</span></h1>
<p>@Model.HeaderText</p>
<div class="cta-group">
<a href="phone:@Model.FormattedPhoneNumber" class="btn btn-primary">Call for Same-Day Service</a>
<a href="@Model.PhoneNumberCallLink" class="btn btn-primary">Call for Same-Day Service</a>
<a href="/book" class="btn btn-secondary">Request an Appointment</a>
</div>
</div>

View File

@@ -21,16 +21,22 @@
<div class="container">
<header class="admin-header">
<div class="tab-container">
<button class="tab-btn @(currentTab == AdminTab.Home ? "active" : "")"
@onclick="() => currentTab = AdminTab.Home">
<button class="tab-btn @(CurrentTab == AdminTab.Home ? "active" : "")"
@onclick="() => CurrentTab = AdminTab.Home">
Home Page
</button>
</div>
<div class="tab-container">
<button class="tab-btn @(CurrentTab == AdminTab.BusinessInfo ? "active" : "")"
@onclick="() => CurrentTab = AdminTab.BusinessInfo">
Business Info
</button>
</div>
</header>
@if (currentTab == AdminTab.Home)
@if (CurrentTab == AdminTab.Home)
{
<EditForm FormName="HomePageForm" Model="HomePageModel" OnValidSubmit="SaveHomePageModel" On class="admin-form home-page-form">
<EditForm FormName="HomePageForm" Model="HomePageModel" OnValidSubmit="SaveHomePageModel" On class="admin-form">
<DataAnnotationsValidator />
<div class="form-section text-center">
@@ -100,9 +106,35 @@
</div>
</EditForm>
}
else
else if (CurrentTab == AdminTab.BusinessInfo)
{
<h1>Another page</h1>
<EditForm FormName="BusinessInfoForm" Model="BusinessInfo" OnValidSubmit="SaveBusinessInfo" On class="admin-form">
<DataAnnotationsValidator />
<div class="form-section text-center">
<h3><i class="icon">🏠</i> Business Info</h3>
<div class="input-group">
<label>Business Name</label>
<InputText @bind-Value="BusinessInfo.Name" class="form-input" />
</div>
<div class="input-group">
<label>Phone Number</label>
<InputText @bind-Value="BusinessInfo.PhoneNumber" class="form-input" />
</div>
<div class="input-group">
<label>Support Email</label>
<InputText @bind-Value="BusinessInfo.SupportEmail" class="form-input" />
</div>
</div>
<div class="admin-footer">
<button type="submit" class="btn btn-save">Save</button>
<button type="button" class="btn btn-revert" @onclick="RevertBusinessInfo">Revert</button>
</div>
</EditForm>
}
</div>
</div>

View File

@@ -1,39 +1,63 @@
namespace ApplianceRepair.Components.Pages.admin
{
public partial class EditPages(HomePageReader homePageReader, ContentCardReader contentCardReader)
public partial class EditPages(HomePageReader homePageReader, ContentCardReader contentCardReader, BusinessConfigReader businessConfigReader)
{
public HomePageModel? HomePageModel;
public BusinessInfoModel? BusinessInfo;
private enum AdminTab { Home, About }
private AdminTab currentTab = AdminTab.Home;
private enum AdminTab { Home, About, BusinessInfo }
private AdminTab CurrentTab = AdminTab.Home;
override
protected async void OnInitialized()
protected override async Task OnInitializedAsync()
{
HomePageModel = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent;
var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
BusinessInfo = new BusinessInfoModel(businessConfig);
HomePageModel = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
}
private async void RevertHomePageModel()
{
HomePageModel = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent;
var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
BusinessInfo = new BusinessInfoModel(businessConfig);
HomePageModel = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
}
private async void RevertBusinessInfo()
{
var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
BusinessInfo = new BusinessInfoModel(businessConfig);
}
private async void SaveHomePageModel()
{
HomePageModel.CreatedAt = DateTime.Now;
HomePageModel.UpdatedAt = DateTime.Now;
await homePageReader.UpdateRecord(HomePageModel);
foreach (var card in HomePageModel.ServicesCards)
{
card.UpdatedAt = DateTime.Now;
await contentCardReader.UpdateRecord(card);
}
foreach (var card in HomePageModel.TrustCards)
{
card.UpdatedAt = DateTime.Now;
await contentCardReader.UpdateRecord(card);
}
}
await homePageReader.AddRecord(HomePageModel);
private async void SaveBusinessInfo()
{
BusinessInfo.UpdatedAt = DateTime.Now;
await businessConfigReader.UpdateRecord(BusinessInfo);
}
private void AddServiceCard()
@@ -42,7 +66,7 @@
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
BelongsToPage = HomePageModel.PageName,
Group = HomePageModel.ContentCardTypes.Service.ToString(),
Group = HomePageModel.ContentCardTypes.Services.ToString(),
Header = "Service Name",
Text = "Short Description"
});