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

Merged
ciphervance merged 6 commits from feat/calendar-view into main 2025-06-21 21:31:00 +00:00
10 changed files with 263 additions and 41 deletions
Showing only changes of commit 557fb72081 - Show all commits

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

@ -14,6 +14,8 @@ public enum LawnCareEventType
public enum MowingPattern
{
[Display(Name = "N/A")]
NotApplicable,
Vertical,
Horizontal,
Diagonal,

View file

@ -2,7 +2,12 @@
ViewData["Title"] = "Lawn Care Calendar";
}
<h1>@ViewData["Title"]</h1>
<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">
@ -27,11 +32,106 @@
@section Scripts {
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script type="text/javascript">
// TODO ADD CALENDAR 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 {
<!-- NEW: Link to your external calendar.css file -->
<link href="~/css/calendar.css" rel="stylesheet" asp-append-version="true" />
}

View file

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

View file

@ -43,6 +43,7 @@
</div>
<div class="form-group">
<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>
</form>
</div>

View file

@ -1,72 +1,74 @@
@model turf_tasker.Models.LawnCareEvent
@{
ViewData["Title"] = "Edit";
ViewData["Title"] = "Edit";
}
<h1>Edit</h1>
<h4>LawnCareEvent</h4>
<h4>Lawn Care Event</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="EventType" class="control-label"></label>
<select asp-for="EventType" class="form-control" asp-items="Html.GetEnumSelectList<LawnCareEventType>()"></select>
<span asp-validation-for="EventType" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="EventDate" class="control-label"></label>
<input asp-for="EventDate" class="form-control" />
<span asp-validation-for="EventDate" class="text-danger"></span>
</div>
<div id="mowingPatternGroup">
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="MowingPattern" class="control-label"></label>
<select asp-for="MowingPattern" class="form-control" asp-items="Html.GetEnumSelectList<MowingPattern>()"></select>
<span asp-validation-for="MowingPattern" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="form-group mb-3">
<label asp-for="Notes" class="control-label"></label>
<textarea asp-for="Notes" class="form-control"></textarea>
<span asp-validation-for="Notes" class="text-danger"></span>
</div>
<div class="form-group mt-3">
<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>
</form>
</div>
</div>
<div>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
<script type="text/javascript">
$(document).ready(function () {
function toggleMowingPattern() {
var selectedEventType = $("#EventType").val();
function toggleMowingPattern() {
var selectedEventType = $("#EventType").val();
if (selectedEventType == '0') { // '0' corresponds to Mowing
$("#mowingPatternGroup").show();
} else {
$("#mowingPatternGroup").hide();
}
if (selectedEventType == '0') { // '0' corresponds to Mowing
$("#mowingPatternGroup").show();
} else {
$("#mowingPatternGroup").hide();
}
}
toggleMowingPattern();
$("#EventType").on("change", function () {
toggleMowingPattern();
$("#EventType").on("change", function () {
toggleMowingPattern();
});
});
</script>
});
</script>
}

View file

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

View file

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

View file

@ -34,3 +34,16 @@ body {
.navbar-custom .nav-link:hover {
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 */
}