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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
bin/
obj/
.vs/
*.db*

23
ApplianceRepair.csproj Normal file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>9bafa5c2-cfce-4fd2-89f6-91b06916b038</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="9.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.12">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.12" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_SelectedScaffolderID>BlazorCRUDScaffolder</_SelectedScaffolderID>
<_SelectedScaffolderCategoryPath>root/Common/Blazor/RazorComponent</_SelectedScaffolderCategoryPath>
<WebStackScaffolding_ControllerDialogWidth>650</WebStackScaffolding_ControllerDialogWidth>
</PropertyGroup>
</Project>

3
ApplianceRepair.slnx Normal file
View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="ApplianceRepair.csproj" />
</Solution>

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

47
Data.cs Normal file
View File

@@ -0,0 +1,47 @@
namespace ApplianceRepair
{
public class HomePageRecord
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? HeaderLine1 { get; set; }
public string? HeaderLine2 { get; set; }
public string? HeaderText { get; set; }
public string? HeaderButton1Text { get; set; }
public string? HeaderButton2Text { get; set; }
public string? HeaderButton1Link { get; set; }
public string? HeaderButton2Link { get; set; }
public string? SecondaryHeaderText { get; set; }
public string? CopyrightText { get; set; }
}
public class ContentCardRecord
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? BelongsToPage { get; set; }
public string? Group { get; set; }
public string? Header { get; set; }
public string? Text { get; set; }
}
public class BusinessConfigRecord
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? Name { get; set; }
public string? PhoneNumber { get; set; }
public string? SupportEmail { get; set; }
}
}

11
DatabaseContext.cs Normal file
View File

@@ -0,0 +1,11 @@
using Microsoft.EntityFrameworkCore;
namespace ApplianceRepair
{
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options)
{
public DbSet<HomePageRecord> HomePage { get; set; }
public DbSet<ContentCardRecord> ContentCards { get; set; }
public DbSet<BusinessConfigRecord> BusinessConfig { get; set; }
}
}

40
Defaults.cs Normal file
View File

@@ -0,0 +1,40 @@
namespace ApplianceRepair
{
public static class Defaults
{
public static readonly HomePageModel DefaultHomePageContent = new()
{
BusinessName = "Appliance Pro",
FormattedPhoneNumber = "(555) 012-3456",
PhoneNumberCallLink = "tel:5550123456",
HeaderLine1 = "Expert Appliance Repair,",
HeaderLine2 = "Done Right Today.",
HeaderText = "Fast, affordable repairs for all major brands. Serving the Greater Metro Area.",
HeaderButton1Text = "Call for Same-Day Service",
HeaderButton1Link = "tel:5550123456",
HeaderButton2Text = "Book Online",
HeaderButton2Link = "#booking",
SecondaryHeaderText = "What We Fix",
ServicesCards =
[
new() { Header = "Refrigerators", Text = "Cooling issues, leaks, and compressor repairs." },
new() { Header = "Washers & Dryers", Text = "Fixing drum issues, drainage, and heating elements." },
new() { Header = "Ovens & Ranges", Text = "Electrical igniters, gas flow, and temperature control." }
],
TrustCards =
[
new() { Header = "90-Day Warranty", Text = "Quality parts and labor guaranteed." },
new() { Header = "Certified Techs", Text = "Licensed, bonded, and background-checked." },
new() { Header = "Fair Pricing", Text = "No hidden fees or diagnostic surprises." }
],
CopyrightText = $"© {DateTime.Now.Year} Appliance Pro. All rights reserved."
};
}
}

View File

@@ -0,0 +1,124 @@
// <auto-generated />
using System;
using ApplianceRepair;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ApplianceRepair.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20260202020516_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ApplianceRepair.BusinessConfigRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<string>("SupportEmail")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("BusinessConfig");
});
modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BelongsToPage")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasColumnType("TEXT");
b.Property<string>("Header")
.HasColumnType("TEXT");
b.Property<string>("Text")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ContentCards");
});
modelBuilder.Entity("ApplianceRepair.HomePageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CopyrightText")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton1Link")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton1Text")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton2Link")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton2Text")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine1")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine2")
.HasColumnType("TEXT");
b.Property<string>("HeaderText")
.HasColumnType("TEXT");
b.Property<string>("SecondaryHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("HomePage");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ApplianceRepair.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BusinessConfig",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
SupportEmail = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BusinessConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ContentCards",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
BelongsToPage = table.Column<string>(type: "TEXT", nullable: true),
Group = table.Column<string>(type: "TEXT", nullable: true),
Header = table.Column<string>(type: "TEXT", nullable: true),
Text = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HomePage",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
HeaderLine1 = table.Column<string>(type: "TEXT", nullable: true),
HeaderLine2 = table.Column<string>(type: "TEXT", nullable: true),
HeaderText = table.Column<string>(type: "TEXT", nullable: true),
HeaderButton1Text = table.Column<string>(type: "TEXT", nullable: true),
HeaderButton2Text = table.Column<string>(type: "TEXT", nullable: true),
HeaderButton1Link = table.Column<string>(type: "TEXT", nullable: true),
HeaderButton2Link = table.Column<string>(type: "TEXT", nullable: true),
SecondaryHeaderText = table.Column<string>(type: "TEXT", nullable: true),
CopyrightText = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HomePage", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BusinessConfig");
migrationBuilder.DropTable(
name: "ContentCards");
migrationBuilder.DropTable(
name: "HomePage");
}
}
}

View File

@@ -0,0 +1,121 @@
// <auto-generated />
using System;
using ApplianceRepair;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ApplianceRepair.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ApplianceRepair.BusinessConfigRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<string>("SupportEmail")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("BusinessConfig");
});
modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BelongsToPage")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasColumnType("TEXT");
b.Property<string>("Header")
.HasColumnType("TEXT");
b.Property<string>("Text")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ContentCards");
});
modelBuilder.Entity("ApplianceRepair.HomePageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CopyrightText")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton1Link")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton1Text")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton2Link")
.HasColumnType("TEXT");
b.Property<string>("HeaderButton2Text")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine1")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine2")
.HasColumnType("TEXT");
b.Property<string>("HeaderText")
.HasColumnType("TEXT");
b.Property<string>("SecondaryHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("HomePage");
});
#pragma warning restore 612, 618
}
}
}

76
Models.cs Normal file
View File

@@ -0,0 +1,76 @@
namespace ApplianceRepair
{
public class ContentCardModel : ContentCardRecord
{
public ContentCardModel()
{
BelongsToPage = string.Empty;
Group = string.Empty;
Header = string.Empty;
Text = string.Empty;
}
public ContentCardModel(ContentCardRecord record)
{
BelongsToPage = record.BelongsToPage;
Group = record.Group;
Header = record.Header;
Text = record.Text;
}
}
public class HomePageModel : HomePageRecord
{
public string BusinessName { get; set; }
public string FormattedPhoneNumber { get; set; }
public string PhoneNumberCallLink { get; set; }
public List<ContentCardModel> ServicesCards { get; set; }
public List<ContentCardModel> TrustCards { get; set; }
public HomePageModel()
{
HeaderLine1 = string.Empty;
HeaderLine2 = string.Empty;
HeaderText = string.Empty;
HeaderButton1Text = string.Empty;
HeaderButton1Link = string.Empty;
HeaderButton2Text = string.Empty;
HeaderButton2Link = string.Empty;
SecondaryHeaderText = string.Empty;
CopyrightText = string.Empty;
BusinessName = "Appliance Pro";
FormattedPhoneNumber = "(555) 555-5555";
PhoneNumberCallLink = $"tel:{FormattedPhoneNumber}";
ServicesCards = [];
TrustCards = [];
}
public HomePageModel(HomePageRecord record)
{
HeaderLine1 = record.HeaderLine1;
HeaderLine2 = record.HeaderLine2;
HeaderText = record.HeaderText;
HeaderButton1Text = record.HeaderButton1Text;
HeaderButton1Link = record.HeaderButton1Link;
HeaderButton2Text = record.HeaderButton2Text;
HeaderButton2Link = record.HeaderButton2Link;
SecondaryHeaderText = record.SecondaryHeaderText;
CopyrightText = record.CopyrightText;
BusinessName = "Appliance Pro";
FormattedPhoneNumber = "(555) 555-5555";
PhoneNumberCallLink = $"tel:{FormattedPhoneNumber}";
ServicesCards = [];
TrustCards = [];
}
}
public class BusinessConfig : BusinessConfigRecord
{
}
}

50
Program.cs Normal file
View File

@@ -0,0 +1,50 @@
using ApplianceRepair;
using ApplianceRepair.Components;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddDbContext<DatabaseContext>(options =>
options.UseSqlite("Data Source=site.db"));
builder.Services.AddMemoryCache();
builder.Services.AddLogging();
builder.Services.AddScoped<BusinessConfigReader>();
builder.Services.AddScoped<ContentCardReader>();
builder.Services.AddScoped<HomePageReader>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
services.GetRequiredService<DatabaseContext>().Database.EnsureCreated();
services.GetRequiredService<DatabaseContext>().Database
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5037",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"secrets1": {
"type": "secrets"
}
}
}

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"secrets1": {
"type": "secrets.user"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"dependencies": {
"secrets1": {
"restored": true,
"restoreTime": "2026-02-02T01:11:05.5247555Z"
}
},
"parameters": {}
}

33
Services.cs Normal file
View File

@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore;
namespace ApplianceRepair
{
public class HomePageReader(DatabaseContext db)
{
public async Task<HomePageRecord?> ReadLatestRecord()
{
return await db.HomePage.OrderByDescending(page => page.Id).FirstAsync();
}
}
public class ContentCardReader(DatabaseContext db)
{
public async Task<List<ContentCardRecord>?> ReadAllByPageAndGroup(string belongsToPage, string group)
{
return await db.ContentCards.Where(card => card.BelongsToPage == belongsToPage && card.Group == group).ToListAsync();
}
public async Task<List<ContentCardRecord>?> ReadAllByPage(string belongsToPage)
{
return await db.ContentCards.Where(card => card.BelongsToPage == belongsToPage).ToListAsync();
}
}
public class BusinessConfigReader(DatabaseContext db)
{
public async Task<BusinessConfigRecord?> ReadLatestRecord()
{
return await db.BusinessConfig.OrderByDescending(page => page.Id).FirstOrDefaultAsync();
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

13
dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.2",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

47
wwwroot/app.css Normal file
View File

@@ -0,0 +1,47 @@
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@600;700&family=Open+Sans:wght@400;600&display=swap');
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.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."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
body {
font-family: 'Open Sans', sans-serif;
line-height: 1.6;
color: #333;
background-color: #f9f9f9;
}