diff --git a/WebApp/Components/Features/Calendar/Index.razor b/WebApp/Components/Features/Calendar/Index.razor index fa9c98a..244776a 100644 --- a/WebApp/Components/Features/Calendar/Index.razor +++ b/WebApp/Components/Features/Calendar/Index.razor @@ -4,7 +4,9 @@ @using WebApp.Models @using WebApp.Services @using Heron.MudCalendar +@using Microsoft.Extensions.Logging @inject IEventOccurrenceService EventOccurrenceService +@inject ILogger Logger @@ -23,7 +25,7 @@ } @@ -39,29 +41,115 @@ private async Task LoadCalendarEvents() { - var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync(); - _calendarItems = occurrences - .Select(occ => new CalendarEventItem(occ)) - .ToList(); + try + { + Logger.LogInformation("Loading calendar events"); + var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync(); + + if (occurrences == null) + { + Logger.LogWarning("Service returned null occurrences"); + _calendarItems = new List(); + return; + } - // Find the next date with events - _calendarDate = GetNextDateWithEvents(); + Logger.LogDebug("Received {Count} occurrences from service", occurrences.Count()); + + var items = new List(); + foreach (var occ in occurrences) + { + try + { + if (occ == null) + { + Logger.LogWarning("Null occurrence found, skipping"); + continue; + } + + if (string.IsNullOrEmpty(occ.Name)) + { + Logger.LogWarning("Occurrence with Id={Id} has null or empty Name", occ.Id); + } + + var calendarItem = new CalendarEventItem(occ, occ.EventDefinition); + items.Add(calendarItem); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error creating CalendarEventItem for occurrence Id={Id}, Name={Name}", + occ?.Id, occ?.Name); + // Continue processing other items + } + } + + _calendarItems = items; + Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences", + _calendarItems.Count, occurrences.Count()); + + // Find the next date with events + _calendarDate = GetNextDateWithEvents(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error loading calendar events"); + _calendarItems = new List(); + } + finally + { + StateHasChanged(); + } } private DateTime GetNextDateWithEvents() { - if (_calendarItems == null || !_calendarItems.Any()) + try { + if (_calendarItems == null || !_calendarItems.Any()) + { + Logger.LogDebug("No calendar items available, returning today's date"); + return DateTime.Today; + } + + var today = DateTime.Today; + var nextEvent = _calendarItems + .Where(item => + { + try + { + return item != null && item.Start.Date >= today; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error checking item date, skipping item"); + return false; + } + }) + .OrderBy(item => item.Start) + .FirstOrDefault(); + + if (nextEvent != null) + { + return nextEvent.Start.Date; + } + + // Fallback to first event if no future events + var firstEvent = _calendarItems + .Where(item => item != null) + .OrderBy(item => item.Start) + .FirstOrDefault(); + + if (firstEvent != null) + { + return firstEvent.Start.Date; + } + + return DateTime.Today; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in GetNextDateWithEvents"); return DateTime.Today; } - - var today = DateTime.Today; - var nextEvent = _calendarItems - .Where(item => item.Start.Date >= today) - .OrderBy(item => item.Start) - .FirstOrDefault(); - - return nextEvent?.Start.Date ?? _calendarItems.OrderBy(item => item.Start).First().Start.Date; } } diff --git a/WebApp/Models/CalendarEventItem.cs b/WebApp/Models/CalendarEventItem.cs index f7a0aaf..b6cab82 100644 --- a/WebApp/Models/CalendarEventItem.cs +++ b/WebApp/Models/CalendarEventItem.cs @@ -24,6 +24,10 @@ public class CalendarEventItem : CalendarItem /// public CalendarEventItem() { + // Initialize base class properties to avoid null reference issues + Text = string.Empty; + Start = DateTime.MinValue; + End = DateTime.MinValue; } public CalendarEventItem(Core.Entities.EventOccurrence occurrence, Core.Entities.EventDefinition? eventDefinition = null) diff --git a/WebApp/Program.cs b/WebApp/Program.cs index 7820822..702e53e 100644 --- a/WebApp/Program.cs +++ b/WebApp/Program.cs @@ -1,5 +1,6 @@ using Data; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.StaticFiles; using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using Serilog; @@ -268,7 +269,31 @@ app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); -app.UseStaticFiles(); +// Configure static files with proper cache headers for Blazor assets +app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = ctx => + { + // Blazor framework files: Use ETags with short cache and revalidation + // This allows caching for performance but ensures fresh files after deployments + // Browser will check ETag on each request (304 Not Modified if unchanged) + if (ctx.File.Name.Contains("_framework") || + ctx.File.Name.Contains("blazor") || + ctx.File.Name.EndsWith(".dll") || + ctx.File.Name.EndsWith(".wasm")) + { + // Cache for 12 hours, but must revalidate (check ETag) before using + // If file hasn't changed, browser gets 304 Not Modified (no download) + // If file changed, browser downloads new version + ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=43200, must-revalidate"); + } + else + { + // Other static files (CSS, images, etc.) can be cached long-term + ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=31536000"); + } + } +}); app.UseAntiforgery(); app.MapRazorComponents()