diff --git a/BlazorApp/BlazorApp.csproj b/BlazorApp/BlazorApp.csproj index b61f689..ccc05a5 100644 --- a/BlazorApp/BlazorApp.csproj +++ b/BlazorApp/BlazorApp.csproj @@ -5,6 +5,7 @@ + all @@ -13,6 +14,7 @@ + diff --git a/BlazorApp/Data/Calendar.cs b/BlazorApp/Data/Calendar.cs index 6ddb5ed..54ba9c0 100644 --- a/BlazorApp/Data/Calendar.cs +++ b/BlazorApp/Data/Calendar.cs @@ -12,6 +12,21 @@ namespace BlazorApp.Data { public class Calendar { + /// + /// Format string used for parsing iCal date-time strings. + /// + private const string DateTimeParseFormat = "yyyyMMddTHHmmssK"; + + /// + /// Format string used for formatting (but not parsing) iCal date-time strings. + /// + public const string DateTimeFormat = "yyyyMMddTHHmmss'Z'"; + + /// + /// Format string used for parsing and formatting iCal date strings. + /// + private const string DateFormat = "yyyyMMdd"; + public int Id { get; set; } [Required] public string Url { get; set; } @@ -28,7 +43,7 @@ namespace BlazorApp.Data private static DateTime _parseDateTime(string s) { - var format = s.Contains('T') ? "yyyyMMddTHHmmssK" : "yyyyMMdd"; + var format = s.Contains('T') ? DateTimeParseFormat : DateFormat; return DateTime.ParseExact(s, format, CultureInfo.InvariantCulture); } @@ -256,15 +271,21 @@ namespace BlazorApp.Data // Build a new URL based on the calendar's URL and the event's collection href. var url = new Uri(new Uri(Url), @event.Href); - var content = new StringContent(@event.ToICal(), Encoding.UTF8, "text/calendar"); + var iCal = @event.ToICal(); + var content = new StringContent(iCal, Encoding.UTF8, "text/calendar"); var response = await client.PutAsync(url, content); // Parse the newly updated event. var body = await response.Content.ReadAsStringAsync(); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($"Could not update event: {body}"); + return @event; + } var events = ParseICal(body); if (events.Count == 0) - return null; + return @event; var newEvent = events[0]; // Extract the updated ETag diff --git a/BlazorApp/Data/Event.cs b/BlazorApp/Data/Event.cs index 4c3da9c..20b4a42 100644 --- a/BlazorApp/Data/Event.cs +++ b/BlazorApp/Data/Event.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; +using NodaTime; namespace BlazorApp.Data { @@ -17,10 +22,23 @@ namespace BlazorApp.Data public string ETag { get; set; } public string Href { get; set; } - public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration; + public DateTime? CalculatedEnd + { + get => DtEnd ?? DtStart + Duration; + set + { + if (Duration != null) + Duration = DtStart - value; + else + DtEnd = value; + } + } private static string _escape(string s) { + if (s == null) + return null; + return s .Replace(@"\", @"\\") .Replace(",", @"\,") @@ -36,24 +54,70 @@ namespace BlazorApp.Data sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n"); sb.Append("BEGIN:VEVENT\r\n"); + var seenNames = new HashSet(); + foreach (var (name, parameters, oldValue) in RawProperties) { - sb.Append(name); - if (parameters != null) - sb.Append($";{parameters}"); - var value = name switch { "SUMMARY" => _escape(Summary), + "DESCRIPTION" => _escape(Description), + "DTSTART" => FormatDate(parameters, DtStart), + "DTEND" => FormatDate(parameters, DtEnd), _ => oldValue }; - sb.Append($":{value}\r\n"); + if (value != null) + { + sb.Append(name); + if (parameters != null) + sb.Append($";{parameters}"); + sb.Append($":{value}\r\n"); + } + + seenNames.Add(name); } + // If one of the event's properties did not exist yet, add it to the end. + if (!seenNames.Contains("SUMMARY") && !string.IsNullOrWhiteSpace(Summary)) + sb.Append($"SUMMARY:{_escape(Summary)}\r\n"); + if (!seenNames.Contains("DESCRIPTION") && !string.IsNullOrWhiteSpace(Description)) + sb.Append($"DESCRIPTION:{_escape(Description)}\r\n"); + if (!seenNames.Contains("DTSTART") && DtStart != null) + sb.Append($"DTSTART:{DtStart?.ToUniversalTime().ToString(Calendar.DateTimeFormat)}\r\n"); + if (!seenNames.Contains("DTEND") && !seenNames.Contains("DURATION") && CalculatedEnd != null) + sb.Append($"DTEND:{DtStart?.ToUniversalTime().ToString(Calendar.DateTimeFormat)}\r\n"); + sb.Append("END:VEVENT\r\n"); sb.Append("END:VCALENDAR\r\n"); return sb.ToString(); } + + private static string FormatDate(string parameters, DateTime? dateTime) + { + if (dateTime == null) + return null; + + DateTimeZone timezone; + if (parameters != null) + { + // If there's parameters, see if we can find a timezone, or default to UTC. + var parameterList = parameters.Split('='); + var timezoneIana = + Array.Find(parameterList, s => s.StartsWith("TZID="))?[5..] ?? "UTC"; + timezone = DateTimeZoneProviders.Tzdb[timezoneIana]; + } + else + { + // If there's no parameters, default to UTC. + timezone = DateTimeZone.Utc; + } + + // Convert the datetime to the used timezone, to avoid changing it. + return Instant + .FromDateTimeUtc(((DateTime) dateTime).ToUniversalTime()) + .InZone(timezone) + .ToString(Calendar.DateTimeFormat, CultureInfo.InvariantCulture); + } } } \ No newline at end of file diff --git a/BlazorApp/Pages/Event.razor b/BlazorApp/Pages/Event.razor index 0d39f1c..d697a81 100644 --- a/BlazorApp/Pages/Event.razor +++ b/BlazorApp/Pages/Event.razor @@ -1,4 +1,5 @@ @page "/calendars/{calendarId:int}/events/{eventUid}" +@using System.Globalization @inject Data.CalendarService _calendarService @inject NavigationManager _navigationManager @@ -14,41 +15,69 @@ } else { - @if (!_editTitle) +

Event

+ +
+ Summary: + @if (_editing != Editing.Summary) + { + + @_event.Summary + + } + else + { +
+ + +
+ } +
+ +
+ Start: + @if (_editing != Editing.DtStart) + { + + @(_event.DtStart?.ToString(DtFormat) ?? "Unknown") + + } + else + { + + + } +
+ +
+ End: + @if (_editing != Editing.CalculatedEnd) + { + + @(_event.CalculatedEnd?.ToString(DtFormat) ?? "Unknown") + + } + else + { + + + } +
+ + @if (_editing != Editing.Description) { -

- @_event.Summary -

+

+ @(_event.Description ?? "No description") +

} else - { - - - } - -
- ETag: @_event.ETag -
- - @if (_event.DtStart != null) {
- Start: @_event.DtStart + +
} - @if (_event.CalculatedEnd != null) - { -
- End: @_event.CalculatedEnd -
- } - - @if (_event.Description != null) - { -

@_event.Description

- } - @if (!_confirmDelete) { @@ -67,13 +96,24 @@ else [Parameter] public string EventUid { get; set; } + const string DtFormat = "dd-MM-yyyy HH:mm:ss"; + private Data.Calendar _calendar; private Data.Event _event; private bool _notFound; - private bool _editTitle; private bool _confirmDelete; + private enum Editing + { + Summary, + DtStart, + CalculatedEnd, + Description, + } + + private Editing? _editing; + protected override async Task OnInitializedAsync() { _calendar = await Data.CalendarService.GetCalendarById(CalendarId); @@ -84,6 +124,7 @@ else private async Task UpdateEvent() { _event = await _calendar.UpdateEvent(_event); + _editing = null; } private async Task DeleteEvent() diff --git a/BlazorApp/Pages/Event.razor.css b/BlazorApp/Pages/Event.razor.css index 1f39632..b97bd55 100644 --- a/BlazorApp/Pages/Event.razor.css +++ b/BlazorApp/Pages/Event.razor.css @@ -1,3 +1,3 @@ -.event-summary > span:hover { +.property-editable:hover { border: 1px solid rgba(0, 0, 0, 0.2); } \ No newline at end of file diff --git a/BlazorApp/Startup.cs b/BlazorApp/Startup.cs index 13c689f..637a8ed 100644 --- a/BlazorApp/Startup.cs +++ b/BlazorApp/Startup.cs @@ -20,7 +20,7 @@ namespace BlazorApp // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - services.AddRazorPages(); + services.AddRazorPages().AddRazorRuntimeCompilation(); services.AddServerSideBlazor(); services.AddSingleton(); }