bunch o stuff
This commit is contained in:
@@ -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>
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user