feat: Enhance UI/UX across multiple views with consistent styling and improved layout

This commit is contained in:
Blake Ridgway 2025-06-21 16:30:59 -05:00
parent 9931687419
commit 36d4ae00dd
16 changed files with 609 additions and 60 deletions

View file

@ -14,19 +14,15 @@ namespace turf_tasker.Controllers
_context = context; _context = context;
} }
// GET: LawnCareEvents
public async Task<IActionResult> Index(string sortOrder, string searchString) public async Task<IActionResult> Index(string sortOrder, string searchString)
{ {
// Use ViewData to pass sort order info to the view
ViewData["DateSortParm"] = String.IsNullOrEmpty(sortOrder) ? "date_desc" : ""; ViewData["DateSortParm"] = String.IsNullOrEmpty(sortOrder) ? "date_desc" : "";
ViewData["TypeSortParm"] = sortOrder == "Type" ? "type_desc" : "Type"; ViewData["TypeSortParm"] = sortOrder == "Type" ? "type_desc" : "Type";
ViewData["CurrentFilter"] = searchString; ViewData["CurrentFilter"] = searchString;
// Start with a base query that can be modified
var events = from e in _context.LawnCareEvents var events = from e in _context.LawnCareEvents
select e; select e;
// Apply the filter if a search string is provided
if (!String.IsNullOrEmpty(searchString)) if (!String.IsNullOrEmpty(searchString))
{ {
events = events.Where(e => events = events.Where(e =>
@ -35,7 +31,6 @@ namespace turf_tasker.Controllers
); );
} }
// Apply the sorting based on the sortOrder parameter
switch (sortOrder) switch (sortOrder)
{ {
case "date_desc": case "date_desc":
@ -51,12 +46,9 @@ namespace turf_tasker.Controllers
events = events.OrderBy(e => e.EventDate); events = events.OrderBy(e => e.EventDate);
break; break;
} }
// Execute the query and return the view with the filtered/sorted list
return View(await events.AsNoTracking().ToListAsync()); return View(await events.AsNoTracking().ToListAsync());
} }
// GET: LawnCareEvents/Details/5
public async Task<IActionResult> Details(int? id) public async Task<IActionResult> Details(int? id)
{ {
if (id == null) if (id == null)
@ -74,15 +66,13 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent); return View(lawnCareEvent);
} }
// GET: LawnCareEvents/Create
// MODIFIED: We now create a model with a default date.
public IActionResult Create() public IActionResult Create()
{ {
var model = new LawnCareEvent { EventDate = DateTime.Now }; var model = new LawnCareEvent { EventDate = DateTime.Now };
return View(model); return View(model);
} }
// POST: LawnCareEvents/Create
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent) public async Task<IActionResult> Create([Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent)
@ -96,7 +86,6 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent); return View(lawnCareEvent);
} }
// GET: LawnCareEvents/Edit/5
public async Task<IActionResult> Edit(int? id) public async Task<IActionResult> Edit(int? id)
{ {
if (id == null) if (id == null)
@ -112,7 +101,6 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent); return View(lawnCareEvent);
} }
// POST: LawnCareEvents/Edit/5
[HttpPost] [HttpPost]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent) public async Task<IActionResult> Edit(int id, [Bind("Id,EventType,EventDate,MowingPattern,Notes")] LawnCareEvent lawnCareEvent)
@ -131,7 +119,7 @@ namespace turf_tasker.Controllers
} }
catch (DbUpdateConcurrencyException) catch (DbUpdateConcurrencyException)
{ {
if (!LawnCareEventExists(lawnCareEvent.Id)) if (!LawnCareEventExists(lawnCareEvent.Id)) // This uses the private method below
{ {
return NotFound(); return NotFound();
} }
@ -145,7 +133,6 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent); return View(lawnCareEvent);
} }
// GET: LawnCareEvents/Delete/5
public async Task<IActionResult> Delete(int? id) public async Task<IActionResult> Delete(int? id)
{ {
if (id == null) if (id == null)
@ -163,7 +150,6 @@ namespace turf_tasker.Controllers
return View(lawnCareEvent); return View(lawnCareEvent);
} }
// POST: LawnCareEvents/Delete/5
[HttpPost, ActionName("Delete")] [HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken] [ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id) public async Task<IActionResult> DeleteConfirmed(int id)
@ -182,5 +168,36 @@ namespace turf_tasker.Controllers
{ {
return _context.LawnCareEvents.Any(e => e.Id == id); return _context.LawnCareEvents.Any(e => e.Id == id);
} }
public IActionResult Calendar()
{
return View();
}
[HttpGet]
public async Task<JsonResult> GetEventsJson()
{
var events = await _context.LawnCareEvents.AsNoTracking().ToListAsync();
var calendarEvents = events.Select(e => new
{
id = e.Id,
title = $"{e.EventType} {((e.EventType == LawnCareEventType.Mowing && e.MowingPattern.HasValue) ? $"({e.MowingPattern})" : "")}",
start = e.EventDate.ToString("yyyy-MM-dd"),
allDay = true,
url = $"/LawnCareEvents/Details/{e.Id}",
color = e.EventType switch
{
LawnCareEventType.Mowing => "#28a745",
LawnCareEventType.Watering => "#007bff",
LawnCareEventType.Fertilizing => "#ffc107",
LawnCareEventType.Aeration => "#6f42c1",
LawnCareEventType.WeedControl => "#dc3545",
_ => "#6c757d"
}
}).ToList();
return Json(calendarEvents);
}
} }
} }

View file

@ -0,0 +1,72 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using turf_tasker.Data;
#nullable disable
namespace turf_tasker.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250619022142_AddNotApplicableMowingPattern")]
partial class AddNotApplicableMowingPattern
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.Entity("turf_tasker.Models.LawnCareEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("EventDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareEvents");
});
modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Category")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareTips");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace turf_tasker.Migrations
{
/// <inheritdoc />
public partial class AddNotApplicableMowingPattern : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View file

@ -0,0 +1,86 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using turf_tasker.Data;
#nullable disable
namespace turf_tasker.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250621212709_AddEventDetails")]
partial class AddEventDetails
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
modelBuilder.Entity("turf_tasker.Models.LawnCareEvent", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<decimal?>("AppliedAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("EventDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<decimal?>("MowerHeightInches")
.HasColumnType("TEXT");
b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER");
b.Property<string>("Notes")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("ProblemObserved")
.HasMaxLength(250)
.HasColumnType("TEXT");
b.Property<string>("ProductUsed")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareEvents");
});
modelBuilder.Entity("turf_tasker.Models.LawnCareTip", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Category")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("LawnCareTips");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,60 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace turf_tasker.Migrations
{
/// <inheritdoc />
public partial class AddEventDetails : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "AppliedAmount",
table: "LawnCareEvents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "MowerHeightInches",
table: "LawnCareEvents",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ProblemObserved",
table: "LawnCareEvents",
type: "TEXT",
maxLength: 250,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ProductUsed",
table: "LawnCareEvents",
type: "TEXT",
maxLength: 200,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AppliedAmount",
table: "LawnCareEvents");
migrationBuilder.DropColumn(
name: "MowerHeightInches",
table: "LawnCareEvents");
migrationBuilder.DropColumn(
name: "ProblemObserved",
table: "LawnCareEvents");
migrationBuilder.DropColumn(
name: "ProductUsed",
table: "LawnCareEvents");
}
}
}

View file

@ -23,12 +23,18 @@ namespace turf_tasker.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<decimal?>("AppliedAmount")
.HasColumnType("TEXT");
b.Property<DateTime>("EventDate") b.Property<DateTime>("EventDate")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<int>("EventType") b.Property<int>("EventType")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<decimal?>("MowerHeightInches")
.HasColumnType("TEXT");
b.Property<int?>("MowingPattern") b.Property<int?>("MowingPattern")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
@ -36,6 +42,14 @@ namespace turf_tasker.Migrations
.HasMaxLength(500) .HasMaxLength(500)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("ProblemObserved")
.HasMaxLength(250)
.HasColumnType("TEXT");
b.Property<string>("ProductUsed")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("LawnCareEvents"); b.ToTable("LawnCareEvents");

View file

@ -9,11 +9,17 @@ public enum LawnCareEventType
Fertilizing, Fertilizing,
Aeration, Aeration,
WeedControl, WeedControl,
PestControl,
DiseaseControl,
Topdressing,
Overseeding,
Other Other
} }
public enum MowingPattern public enum MowingPattern
{ {
[Display(Name = "N/A")]
NotApplicable,
Vertical, Vertical,
Horizontal, Horizontal,
Diagonal, Diagonal,
@ -30,17 +36,30 @@ public class LawnCareEvent
[Display(Name = "Activity Type")] [Display(Name = "Activity Type")]
public LawnCareEventType EventType { get; set; } public LawnCareEventType EventType { get; set; }
// --- THIS IS THE LINE TO CHECK ---
// Make sure this property exists and is spelled correctly.
[Required] [Required]
[DataType(DataType.Date)] [DataType(DataType.Date)]
[Display(Name = "Date")] [Display(Name = "Date")]
public DateTime EventDate { get; set; } public DateTime EventDate { get; set; }
// ---------------------------------
[Display(Name = "Mowing Pattern")] [Display(Name = "Mowing Pattern")]
public MowingPattern? MowingPattern { get; set; } public MowingPattern? MowingPattern { get; set; }
[Display(Name = "Product Used")]
[StringLength(200)]
public string? ProductUsed { get; set; }
[Display(Name = "Applied Amount")]
[Range(0.01, 1000.0, ErrorMessage = "Amount must be positive.")] // Example range
public decimal? AppliedAmount { get; set; }
[Display(Name = "Mower Height (inches)")]
[Range(0.5, 6.0, ErrorMessage = "Height must be between 0.5 and 6 inches.")]
public decimal? MowerHeightInches { get; set; }
[Display(Name = "Problem Observed")]
[StringLength(250)]
public string? ProblemObserved { get; set; }
[StringLength(500)] [StringLength(500)]
public string? Notes { get; set; } public string? Notes { get; set; }
} }

View file

@ -0,0 +1,137 @@
@{
ViewData["Title"] = "Lawn Care Calendar";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>@ViewData["Title"]</h1>
<a asp-action="Create" asp-controller="LawnCareEvents" class="btn btn-primary">
<i class="bi bi-plus-circle-fill"></i> Create New Event
</a>
</div>
<div class="calendar-container">
<div class="calendar-header">
<button id="prevMonth" class="btn btn-secondary">&lt; Prev</button>
<h2 id="currentMonthYear"></h2>
<button id="nextMonth" class="btn btn-secondary">Next &gt;</button>
</div>
<div class="calendar-grid">
<div class="day-name">Sun</div>
<div class="day-name">Mon</div>
<div class="day-name">Tue</div>
<div class="day-name">Wed</div>
<div class="day-name">Thu</div>
<div class="day-name">Fri</div>
<div class="day-name">Sat</div>
</div>
<div id="calendarDays" class="calendar-grid">
</div>
</div>
@section Scripts {
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
const calendarDaysEl = $('#calendarDays');
const currentMonthYearEl = $('#currentMonthYear');
const prevMonthBtn = $('#prevMonth');
const nextMonthBtn = $('#nextMonth');
let currentMonth = new Date().getMonth();
let currentYear = new Date().getFullYear();
let allEvents = [];
function renderCalendar(month, year) {
calendarDaysEl.empty();
currentMonthYearEl.text(new Date(year, month).toLocaleString('default', { month: 'long', year: 'numeric' }));
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
const daysInMonth = lastDayOfMonth.getDate();
const startDayOfWeek = firstDayOfMonth.getDay();
const prevMonthLastDay = new Date(year, month, 0).getDate();
for (let i = startDayOfWeek; i > 0; i--) {
const dayNum = prevMonthLastDay - i + 1;
calendarDaysEl.append(`<div class="calendar-day other-month"><span class="day-number">${dayNum}</span></div>`);
}
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, month, day);
const dateString = date.toISOString().split('T')[0];
let dayHtml = `<div class="calendar-day" data-date="${dateString}">`;
dayHtml += `<span class="day-number">${day}</span>`;
const eventsOnThisDay = allEvents.filter(event => event.start === dateString);
eventsOnThisDay.forEach(event => {
const typeClass = event.title.split(' ')[0].toLowerCase().replace(/[^a-z0-9]/g, '');
dayHtml += `<div class="event-indicator ${typeClass}" data-event-id="${event.id}" data-event-url="${event.url}">${event.title}</div>`;
});
dayHtml += `</div>`;
calendarDaysEl.append(dayHtml);
}
const totalDaysDisplayed = startDayOfWeek + daysInMonth;
const remainingCells = 42 - totalDaysDisplayed;
for (let i = 1; i <= remainingCells && totalDaysDisplayed + i <= 42; i++) {
calendarDaysEl.append(`<div class="calendar-day other-month"><span class="day-number">${i}</span></div>`);
}
$('.calendar-day .event-indicator').on('click', function (e) {
e.stopPropagation();
const url = $(this).data('event-url');
if (url) {
window.location.href = url;
}
});
$('.calendar-day:not(.other-month)').on('click', function () {
const date = $(this).data('date');
window.location.href = `/LawnCareEvents/Create?date=${date}`;
});
}
async function fetchEvents() {
try {
const response = await fetch('/LawnCareEvents/GetEventsJson');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
allEvents = await response.json();
renderCalendar(currentMonth, currentYear);
} catch (error) {
console.error('Error fetching events:', error);
}
}
prevMonthBtn.on('click', function () {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar(currentMonth, currentYear);
});
nextMonthBtn.on('click', function () {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar(currentMonth, currentYear);
});
fetchEvents();
});
</script>
}
@section Styles {
<link href="~/css/calendar.css" rel="stylesheet" asp-append-version="true" />
}

View file

@ -39,7 +39,11 @@
<form asp-action="Delete"> <form asp-action="Delete">
<input type="hidden" asp-for="Id" /> <input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> | <input type="submit" value="Delete" class="btn btn-danger" />
<a asp-action="Index">Back to List</a>
</form> </form>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
</div> </div>

View file

@ -43,6 +43,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" /> <input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger ms-2">Delete</a>
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,55 +1,57 @@
@model turf_tasker.Models.LawnCareEvent @model turf_tasker.Models.LawnCareEvent
@{ @{
ViewData["Title"] = "Edit"; ViewData["Title"] = "Edit";
} }
<h1>Edit</h1> <h1>Edit</h1>
<h4>LawnCareEvent</h4> <h4>Lawn Care Event</h4>
<hr /> <hr />
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<form asp-action="Edit"> <form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div> <div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" /> <input type="hidden" asp-for="Id" />
<div class="form-group"> <div class="form-group mb-3">
<label asp-for="EventType" class="control-label"></label> <label asp-for="EventType" class="control-label"></label>
<select asp-for="EventType" class="form-control" asp-items="Html.GetEnumSelectList<LawnCareEventType>()"></select> <select asp-for="EventType" class="form-control" asp-items="Html.GetEnumSelectList<LawnCareEventType>()"></select>
<span asp-validation-for="EventType" class="text-danger"></span> <span asp-validation-for="EventType" class="text-danger"></span>
</div> </div>
<div class="form-group"> <div class="form-group mb-3">
<label asp-for="EventDate" class="control-label"></label> <label asp-for="EventDate" class="control-label"></label>
<input asp-for="EventDate" class="form-control" /> <input asp-for="EventDate" class="form-control" />
<span asp-validation-for="EventDate" class="text-danger"></span> <span asp-validation-for="EventDate" class="text-danger"></span>
</div> </div>
<div id="mowingPatternGroup"> <div id="mowingPatternGroup">
<div class="form-group"> <div class="form-group mb-3">
<label asp-for="MowingPattern" class="control-label"></label> <label asp-for="MowingPattern" class="control-label"></label>
<select asp-for="MowingPattern" class="form-control" asp-items="Html.GetEnumSelectList<MowingPattern>()"></select> <select asp-for="MowingPattern" class="form-control" asp-items="Html.GetEnumSelectList<MowingPattern>()"></select>
<span asp-validation-for="MowingPattern" class="text-danger"></span> <span asp-validation-for="MowingPattern" class="text-danger"></span>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group mb-3">
<label asp-for="Notes" class="control-label"></label> <label asp-for="Notes" class="control-label"></label>
<textarea asp-for="Notes" class="form-control"></textarea> <textarea asp-for="Notes" class="form-control"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span> <span asp-validation-for="Notes" class="text-danger"></span>
</div> </div>
<div class="form-group mt-3"> <div class="form-group mt-3">
<input type="submit" value="Save" class="btn btn-primary" /> <input type="submit" value="Save" class="btn btn-primary" />
<!-- NEW: Add a space/margin and use Razor syntax to pass the Id -->
<a asp-action="Delete" asp-route-id="@Model.Id" class="btn btn-danger">Delete</a>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<div> <div class="mt-3">
<a asp-action="Index">Back to List</a> <a asp-action="Index">Back to List</a>
</div> </div>
@section Scripts { @section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");} @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function () { $(document).ready(function () {
function toggleMowingPattern() { function toggleMowingPattern() {
@ -68,5 +70,5 @@
toggleMowingPattern(); toggleMowingPattern();
}); });
}); });
</script> </script>
} }

View file

@ -4,11 +4,14 @@
ViewData["Title"] = "Lawn Care Log"; ViewData["Title"] = "Lawn Care Log";
} }
<h1>@ViewData["Title"]</h1> <div class="text-center">
<h1>@ViewData["Title"]</h1>
<p> <p>
<a asp-action="Create" class="btn btn-primary">Create New Event</a> <a asp-action="Create" class="btn btn-primary">Create New Event</a>
</p> <a asp-action="Calendar" class="btn btn-info ms-2">View Calendar</a>
</p>
</div>
<!-- Add the search form --> <!-- Add the search form -->
<form asp-action="Index" method="get"> <form asp-action="Index" method="get">
@ -16,8 +19,8 @@
<p> <p>
Find by type or note: Find by type or note:
<input type="text" name="searchString" value="@ViewData["CurrentFilter"]" /> <input type="text" name="searchString" value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-secondary" /> | <input type="submit" value="Search" class="btn btn-secondary" />
<a asp-action="Index">Back to Full List</a> <a asp-action="Index" class="btn btn-primary">Reset Filter</a>
</p> </p>
</div> </div>
</form> </form>
@ -63,9 +66,11 @@
@Html.DisplayFor(modelItem => item.Notes) @Html.DisplayFor(modelItem => item.Notes)
</td> </td>
<td> <td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> | <div class="d-flex flex-wrap gap-2">
<a asp-action="Details" asp-route-id="@item.Id">Details</a> | <a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a> <a asp-action="Details" asp-route-id="@item.Id" class="btn btn-sm btn-outline-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</td> </td>
</tr> </tr>
} }

View file

@ -7,7 +7,7 @@
<h1>Index</h1> <h1>Index</h1>
<p> <p>
<a asp-action="Create">Create New</a> <a asp-action="Create" class="btn btn-primary">Create New Tip</a>
</p> </p>
<table class="table"> <table class="table">
<thead> <thead>
@ -36,10 +36,11 @@
<td> <td>
@Html.DisplayFor(modelItem => item.Content) @Html.DisplayFor(modelItem => item.Content)
</td> </td>
<td> <td class="col-actions">
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> | <div class="d-flex flex-wrap gap-2">
<a asp-action="Details" asp-route-id="@item.Id">Details</a> | <a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-sm btn-outline-primary">Edit</a>
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a> <a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-sm btn-outline-danger">Delete</a>
</div>
</td> </td>
</tr> </tr>
} }

View file

@ -5,6 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - MowLog</title> <title>@ViewData["Title"] - MowLog</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
@await RenderSectionAsync("Scripts", required: false)
@await RenderSectionAsync("Styles", required: false)
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/MowLog.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="~/MowLog.styles.css" asp-append-version="true" />
</head> </head>
@ -46,6 +48,5 @@
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script> <script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body> </body>
</html> </html>

95
wwwroot/css/calendar.css Normal file
View file

@ -0,0 +1,95 @@
.calendar-container {
max-width: 900px;
margin: 20px auto;
font-family: Arial, sans-serif;
border: 1px solid #ddd;
border-radius: 5px;
overflow: hidden;
background-color: #fff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #f8f9fa;
padding: 15px;
border-bottom: 1px solid #ddd;
}
.calendar-header h2 {
margin: 0;
font-size: 1.5em;
color: #333;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 1px;
background-color: #ddd;
border-bottom: 1px solid #ddd;
}
.day-name {
background-color: #e9ecef;
padding: 10px 5px;
text-align: center;
font-weight: bold;
color: #555;
font-size: 0.9em;
}
.calendar-day {
background-color: #fff;
padding: 10px 5px;
min-height: 100px;
text-align: right;
position: relative;
cursor: pointer;
transition: background-color 0.2s ease;
box-sizing: border-box;
border: 1px solid #eee;
}
.calendar-day:hover {
background-color: #f0f0f0;
}
.calendar-day.other-month {
background-color: #f9f9f9;
color: #ccc;
}
.calendar-day .day-number {
font-weight: bold;
font-size: 1.2em;
color: #333;
position: absolute;
top: 5px;
right: 5px;
}
.calendar-day .event-indicator {
background-color: #28a745;
color: white;
font-size: 0.75em;
padding: 2px 5px;
border-radius: 3px;
margin-top: 25px;
display: inline-block;
margin-left: 5px;
margin-right: 5px;
}
.event-indicator.mowing { background-color: #28a745; }
.event-indicator.watering { background-color: #007bff; }
.event-indicator.fertilizing { background-color: #ffc107; }
.event-indicator.aeration { background-color: #6f42c1; }
.event-indicator.weedcontrol { background-color: #dc3545; }
.event-indicator.other { background-color: #6c757d; }
.card.h-100 {
height: 100% !important;
}

View file

@ -34,3 +34,16 @@ body {
.navbar-custom .nav-link:hover { .navbar-custom .nav-link:hover {
color: #d4d4d4; /* A light gray for hover effect */ color: #d4d4d4; /* A light gray for hover effect */
} }
/* For tables, to control column widths */
.table .col-content {
max-width: 400px; /* Adjust as needed */
word-wrap: break-word; /* Break long words */
white-space: normal; /* Allow text to wrap normally */
}
.table .col-actions {
width: 150px; /* Fixed width for action buttons, adjust as needed */
min-width: 120px; /* Ensure minimum space */
white-space: nowrap; /* Keep buttons on one line, prevent wrapping */
}