From 5267701e32febc5a2b58f01c3898ba4e83d90ff3 Mon Sep 17 00:00:00 2001 From: slinky55 Date: Tue, 3 Feb 2026 16:55:37 -0600 Subject: [PATCH] initial commit --- .gitignore | 4 + ApplianceRepair.csproj | 23 +++ ApplianceRepair.csproj.user | 8 + ApplianceRepair.slnx | 3 + Components/App.razor | 19 +++ Components/Layout/MainLayout.razor | 9 ++ Components/Layout/MainLayout.razor.css | 20 +++ Components/Pages/Error.razor | 36 +++++ Components/Pages/Home.Razor.cs | 48 ++++++ Components/Pages/Home.razor | 67 ++++++++ Components/Pages/Home.razor.css | 148 ++++++++++++++++++ Components/Routes.razor | 6 + Components/_Imports.razor | 10 ++ Data.cs | 47 ++++++ DatabaseContext.cs | 11 ++ Defaults.cs | 40 +++++ .../20260202020516_InitialCreate.Designer.cs | 124 +++++++++++++++ Migrations/20260202020516_InitialCreate.cs | 86 ++++++++++ Migrations/DatabaseContextModelSnapshot.cs | 121 ++++++++++++++ Models.cs | 76 +++++++++ Program.cs | 50 ++++++ Properties/launchSettings.json | 14 ++ Properties/serviceDependencies.json | 7 + Properties/serviceDependencies.local.json | 7 + .../serviceDependencies.local.json.user | 9 ++ Services.cs | 33 ++++ appsettings.Development.json | 8 + appsettings.json | 9 ++ dotnet-tools.json | 13 ++ wwwroot/app.css | 47 ++++++ 30 files changed, 1103 insertions(+) create mode 100644 .gitignore create mode 100644 ApplianceRepair.csproj create mode 100644 ApplianceRepair.csproj.user create mode 100644 ApplianceRepair.slnx create mode 100644 Components/App.razor create mode 100644 Components/Layout/MainLayout.razor create mode 100644 Components/Layout/MainLayout.razor.css create mode 100644 Components/Pages/Error.razor create mode 100644 Components/Pages/Home.Razor.cs create mode 100644 Components/Pages/Home.razor create mode 100644 Components/Pages/Home.razor.css create mode 100644 Components/Routes.razor create mode 100644 Components/_Imports.razor create mode 100644 Data.cs create mode 100644 DatabaseContext.cs create mode 100644 Defaults.cs create mode 100644 Migrations/20260202020516_InitialCreate.Designer.cs create mode 100644 Migrations/20260202020516_InitialCreate.cs create mode 100644 Migrations/DatabaseContextModelSnapshot.cs create mode 100644 Models.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Properties/serviceDependencies.json create mode 100644 Properties/serviceDependencies.local.json create mode 100644 Properties/serviceDependencies.local.json.user create mode 100644 Services.cs create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 dotnet-tools.json create mode 100644 wwwroot/app.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a87e516 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +obj/ +.vs/ +*.db* \ No newline at end of file diff --git a/ApplianceRepair.csproj b/ApplianceRepair.csproj new file mode 100644 index 0000000..b79c5f3 --- /dev/null +++ b/ApplianceRepair.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + 9bafa5c2-cfce-4fd2-89f6-91b06916b038 + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/ApplianceRepair.csproj.user b/ApplianceRepair.csproj.user new file mode 100644 index 0000000..c05f89c --- /dev/null +++ b/ApplianceRepair.csproj.user @@ -0,0 +1,8 @@ + + + + <_SelectedScaffolderID>BlazorCRUDScaffolder + <_SelectedScaffolderCategoryPath>root/Common/Blazor/RazorComponent + 650 + + \ No newline at end of file diff --git a/ApplianceRepair.slnx b/ApplianceRepair.slnx new file mode 100644 index 0000000..a660ae8 --- /dev/null +++ b/ApplianceRepair.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Components/App.razor b/Components/App.razor new file mode 100644 index 0000000..5a7185f --- /dev/null +++ b/Components/App.razor @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor new file mode 100644 index 0000000..96fbbe6 --- /dev/null +++ b/Components/Layout/MainLayout.razor @@ -0,0 +1,9 @@ +@inherits LayoutComponentBase + +@Body + +
+ An unhandled error has occurred. + Reload + 🗙 +
diff --git a/Components/Layout/MainLayout.razor.css b/Components/Layout/MainLayout.razor.css new file mode 100644 index 0000000..60cec92 --- /dev/null +++ b/Components/Layout/MainLayout.razor.css @@ -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; + } diff --git a/Components/Pages/Error.razor b/Components/Pages/Error.razor new file mode 100644 index 0000000..576cc2d --- /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.cs b/Components/Pages/Home.Razor.cs new file mode 100644 index 0000000..fce5cda --- /dev/null +++ b/Components/Pages/Home.Razor.cs @@ -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); + } + } + + } +} diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor new file mode 100644 index 0000000..8f137dc --- /dev/null +++ b/Components/Pages/Home.razor @@ -0,0 +1,67 @@ +@page "/" +@rendermode InteractiveServer + +Home + +@if (Model == null) +{ +

Loading...

+} +else +{ + + +
+
+

@Model.HeaderLine1
@Model.HeaderLine2

+

@Model.HeaderText

+ +
+
+ +
+
+

@Model.SecondaryHeaderText

+
+ @if (Model.ServicesCards != null) + { + foreach (var serviceCard in Model.ServicesCards) + { +
+

@serviceCard.Header

+

@serviceCard.Text

+
+ } + } +
+
+
+ +
+
+ @if (Model.TrustCards != null) + { + foreach (var trustCard in Model.TrustCards) + { +
+ @trustCard.Header +

@trustCard.Text

+
+ } + } +
+
+ +
+

@Model.CopyrightText

+
+} + diff --git a/Components/Pages/Home.razor.css b/Components/Pages/Home.razor.css new file mode 100644 index 0000000..fee8b4d --- /dev/null +++ b/Components/Pages/Home.razor.css @@ -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; + } +} diff --git a/Components/Routes.razor b/Components/Routes.razor new file mode 100644 index 0000000..f756e19 --- /dev/null +++ b/Components/Routes.razor @@ -0,0 +1,6 @@ + + + + + + diff --git a/Components/_Imports.razor b/Components/_Imports.razor new file mode 100644 index 0000000..9a46b11 --- /dev/null +++ b/Components/_Imports.razor @@ -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 diff --git a/Data.cs b/Data.cs new file mode 100644 index 0000000..51d99ff --- /dev/null +++ b/Data.cs @@ -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; } + } +} diff --git a/DatabaseContext.cs b/DatabaseContext.cs new file mode 100644 index 0000000..54f598c --- /dev/null +++ b/DatabaseContext.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace ApplianceRepair +{ + public class DatabaseContext(DbContextOptions options) : DbContext(options) + { + public DbSet HomePage { get; set; } + public DbSet ContentCards { get; set; } + public DbSet BusinessConfig { get; set; } + } +} diff --git a/Defaults.cs b/Defaults.cs new file mode 100644 index 0000000..f352f13 --- /dev/null +++ b/Defaults.cs @@ -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." + }; + } +} diff --git a/Migrations/20260202020516_InitialCreate.Designer.cs b/Migrations/20260202020516_InitialCreate.Designer.cs new file mode 100644 index 0000000..60f1b63 --- /dev/null +++ b/Migrations/20260202020516_InitialCreate.Designer.cs @@ -0,0 +1,124 @@ +// +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 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.12"); + + modelBuilder.Entity("ApplianceRepair.BusinessConfigRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("SupportEmail") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BusinessConfig"); + }); + + modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BelongsToPage") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasColumnType("TEXT"); + + b.Property("Header") + .HasColumnType("TEXT"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ContentCards"); + }); + + modelBuilder.Entity("ApplianceRepair.HomePageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CopyrightText") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("HeaderButton1Link") + .HasColumnType("TEXT"); + + b.Property("HeaderButton1Text") + .HasColumnType("TEXT"); + + b.Property("HeaderButton2Link") + .HasColumnType("TEXT"); + + b.Property("HeaderButton2Text") + .HasColumnType("TEXT"); + + b.Property("HeaderLine1") + .HasColumnType("TEXT"); + + b.Property("HeaderLine2") + .HasColumnType("TEXT"); + + b.Property("HeaderText") + .HasColumnType("TEXT"); + + b.Property("SecondaryHeaderText") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HomePage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20260202020516_InitialCreate.cs b/Migrations/20260202020516_InitialCreate.cs new file mode 100644 index 0000000..5dfe9ad --- /dev/null +++ b/Migrations/20260202020516_InitialCreate.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ApplianceRepair.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BusinessConfig", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + SupportEmail = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_BusinessConfig", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "ContentCards", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + BelongsToPage = table.Column(type: "TEXT", nullable: true), + Group = table.Column(type: "TEXT", nullable: true), + Header = table.Column(type: "TEXT", nullable: true), + Text = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_ContentCards", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "HomePage", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + HeaderLine1 = table.Column(type: "TEXT", nullable: true), + HeaderLine2 = table.Column(type: "TEXT", nullable: true), + HeaderText = table.Column(type: "TEXT", nullable: true), + HeaderButton1Text = table.Column(type: "TEXT", nullable: true), + HeaderButton2Text = table.Column(type: "TEXT", nullable: true), + HeaderButton1Link = table.Column(type: "TEXT", nullable: true), + HeaderButton2Link = table.Column(type: "TEXT", nullable: true), + SecondaryHeaderText = table.Column(type: "TEXT", nullable: true), + CopyrightText = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HomePage", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BusinessConfig"); + + migrationBuilder.DropTable( + name: "ContentCards"); + + migrationBuilder.DropTable( + name: "HomePage"); + } + } +} diff --git a/Migrations/DatabaseContextModelSnapshot.cs b/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 0000000..5dbb96e --- /dev/null +++ b/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,121 @@ +// +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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("SupportEmail") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("BusinessConfig"); + }); + + modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BelongsToPage") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Group") + .HasColumnType("TEXT"); + + b.Property("Header") + .HasColumnType("TEXT"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ContentCards"); + }); + + modelBuilder.Entity("ApplianceRepair.HomePageRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CopyrightText") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("HeaderButton1Link") + .HasColumnType("TEXT"); + + b.Property("HeaderButton1Text") + .HasColumnType("TEXT"); + + b.Property("HeaderButton2Link") + .HasColumnType("TEXT"); + + b.Property("HeaderButton2Text") + .HasColumnType("TEXT"); + + b.Property("HeaderLine1") + .HasColumnType("TEXT"); + + b.Property("HeaderLine2") + .HasColumnType("TEXT"); + + b.Property("HeaderText") + .HasColumnType("TEXT"); + + b.Property("SecondaryHeaderText") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HomePage"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Models.cs b/Models.cs new file mode 100644 index 0000000..97420c7 --- /dev/null +++ b/Models.cs @@ -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 ServicesCards { get; set; } + public List 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 + { + + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..8a4698f --- /dev/null +++ b/Program.cs @@ -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(options => + options.UseSqlite("Data Source=site.db")); + +builder.Services.AddMemoryCache(); +builder.Services.AddLogging(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + try + { + services.GetRequiredService().Database.EnsureCreated(); + services.GetRequiredService().Database + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + 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() + .AddInteractiveServerRenderMode(); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..0c98c9a --- /dev/null +++ b/Properties/launchSettings.json @@ -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" + } + } + } + } diff --git a/Properties/serviceDependencies.json b/Properties/serviceDependencies.json new file mode 100644 index 0000000..a4e7aa3 --- /dev/null +++ b/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets" + } + } +} \ No newline at end of file diff --git a/Properties/serviceDependencies.local.json b/Properties/serviceDependencies.local.json new file mode 100644 index 0000000..09b109b --- /dev/null +++ b/Properties/serviceDependencies.local.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "secrets1": { + "type": "secrets.user" + } + } +} \ No newline at end of file diff --git a/Properties/serviceDependencies.local.json.user b/Properties/serviceDependencies.local.json.user new file mode 100644 index 0000000..8357ec4 --- /dev/null +++ b/Properties/serviceDependencies.local.json.user @@ -0,0 +1,9 @@ +{ + "dependencies": { + "secrets1": { + "restored": true, + "restoreTime": "2026-02-02T01:11:05.5247555Z" + } + }, + "parameters": {} +} \ No newline at end of file diff --git a/Services.cs b/Services.cs new file mode 100644 index 0000000..70cdaf7 --- /dev/null +++ b/Services.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; + +namespace ApplianceRepair +{ + public class HomePageReader(DatabaseContext db) + { + public async Task ReadLatestRecord() + { + return await db.HomePage.OrderByDescending(page => page.Id).FirstAsync(); + } + } + + public class ContentCardReader(DatabaseContext db) + { + public async Task?> ReadAllByPageAndGroup(string belongsToPage, string group) + { + return await db.ContentCards.Where(card => card.BelongsToPage == belongsToPage && card.Group == group).ToListAsync(); + } + + public async Task?> ReadAllByPage(string belongsToPage) + { + return await db.ContentCards.Where(card => card.BelongsToPage == belongsToPage).ToListAsync(); + } + } + + public class BusinessConfigReader(DatabaseContext db) + { + public async Task ReadLatestRecord() + { + return await db.BusinessConfig.OrderByDescending(page => page.Id).FirstOrDefaultAsync(); + } + } +} diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /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..10f68b8 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..a74bf0b --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.2", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/wwwroot/app.css b/wwwroot/app.css new file mode 100644 index 0000000..f9c1cfc --- /dev/null +++ b/wwwroot/app.css @@ -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; +} \ No newline at end of file