initial commit

This commit is contained in:
2026-02-03 16:55:37 -06:00
commit 5267701e32
30 changed files with 1103 additions and 0 deletions

19
Components/App.razor Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["ApplianceRepair.styles.css"]" />
<ImportMap />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,9 @@
@inherits LayoutComponentBase
@Body
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@@ -0,0 +1,20 @@
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

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

View File

@@ -0,0 +1,48 @@
using Microsoft.Extensions.Caching.Memory;
namespace ApplianceRepair.Components.Pages
{
public partial class Home(IMemoryCache cache, HomePageReader homePageReader, ContentCardReader contentCardReader)
{
private HomePageModel? Model;
protected override async Task OnInitializedAsync()
{
if (!cache.TryGetValue(nameof(HomePageModel), out Model))
{
Model = Defaults.DefaultHomePageContent;
var homePageRecord = await homePageReader.ReadLatestRecord();
if (homePageRecord != null)
{
Model = new HomePageModel(homePageRecord);
var serviceCardRecords = await contentCardReader.ReadAllByPageAndGroup(nameof(Home), "Services");
if (serviceCardRecords != null)
{
foreach (var record in serviceCardRecords)
{
Model.ServicesCards!.Add(new ContentCardModel(record));
}
}
var trustCardRecords = await contentCardReader.ReadAllByPageAndGroup(nameof(Home), "Trust");
if (trustCardRecords != null)
{
foreach (var record in trustCardRecords)
{
Model.TrustCards!.Add(new ContentCardModel(record));
}
}
}
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(24))
.SetSlidingExpiration(TimeSpan.FromHours(2));
cache.Set(nameof(HomePageModel), Model, cacheOptions);
}
}
}
}

View File

@@ -0,0 +1,67 @@
@page "/"
@rendermode InteractiveServer
<PageTitle>Home</PageTitle>
@if (Model == null)
{
<p>Loading...</p>
}
else
{
<nav class="navbar">
<div class="container">
<div class="logo">@Model.BusinessName</div>
<a href="@Model.PhoneNumberCallLink" class="nav-phone">📞 @Model.FormattedPhoneNumber</a>
</div>
</nav>
<header class="hero">
<div class="container">
<h1>@Model.HeaderLine1 <br><span>@Model.HeaderLine2</span></h1>
<p>@Model.HeaderText</p>
<div class="cta-group">
<a href="@Model.HeaderButton1Link" class="btn btn-primary">@Model.HeaderButton1Text</a>
<a href="@Model.HeaderButton2Link" class="btn btn-secondary">@Model.HeaderButton2Text</a>
</div>
</div>
</header>
<section class="services">
<div class="container">
<h2 class="section-title">@Model.SecondaryHeaderText</h2>
<div class="service-grid">
@if (Model.ServicesCards != null)
{
foreach (var serviceCard in Model.ServicesCards)
{
<div class="service-card">
<h3>@serviceCard.Header</h3>
<p>@serviceCard.Text</p>
</div>
}
}
</div>
</div>
</section>
<section class="trust">
<div class="container">
@if (Model.TrustCards != null)
{
foreach (var trustCard in Model.TrustCards)
{
<div class="trust-item">
<strong>@trustCard.Header</strong>
<p>@trustCard.Text</p>
</div>
}
}
</div>
</section>
<footer>
<p>@Model.CopyrightText</p>
</footer>
}

View File

@@ -0,0 +1,148 @@
/* General Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
/* Navbar */
.navbar {
background: #fff;
padding: 1rem 0;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.navbar .container {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: #0056b3;
}
.logo span {
color: #e63946;
}
.nav-phone {
text-decoration: none;
color: #0056b3;
font-weight: bold;
}
/* Hero Section */
.hero {
background: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.6)), url('https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?auto=format&fit=crop&w=1200&q=80');
background-size: cover;
background-position: center;
color: #fff;
padding: 100px 0;
text-align: center;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
}
.hero h1 span {
color: #4cc9f0;
}
/* Buttons */
.btn {
display: inline-block;
padding: 12px 25px;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
margin: 10px;
transition: 0.3s;
}
.btn-primary {
background: #e63946;
color: #fff;
}
.btn-secondary {
background: #fff;
color: #333;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-2px);
}
/* Services */
.services {
padding: 60px 0;
}
.section-title {
text-align: center;
margin-bottom: 40px;
font-size: 2rem;
}
.service-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.service-card {
background: #fff;
padding: 30px;
border-radius: 8px;
text-align: center;
border-bottom: 4px solid #0056b3;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
}
/* Trust Section */
.trust {
background: #0056b3;
color: #fff;
padding: 40px 0;
text-align: center;
}
.trust .container {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.trust-item {
margin: 20px;
}
footer {
text-align: center;
padding: 20px;
font-size: 0.9rem;
color: #777;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.hero h1 {
font-size: 2rem;
}
}

6
Components/Routes.razor Normal file
View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

10
Components/_Imports.razor Normal file
View File

@@ -0,0 +1,10 @@
@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 ApplianceRepair
@using ApplianceRepair.Components