Skip to content

Commit 7e7d09b

Browse files
authored
Add personal calendar event length, title, and location modifications (#203)
1 parent 9dfe4c8 commit 7e7d09b

14 files changed

Lines changed: 251 additions & 89 deletions

StartSch.Wasm/Components/EventSettingsDialog.razor

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
</md-icon-button>
2323

2424
<h2>
25-
@EditContext.EventContext.OriginalEvent.Title
25+
@RenderStringDiff(originalEvent.Title, modifiedEvent.Title)
2626
</h2>
2727
<ul class="property-list">
2828
@if (modifiedEvent is { SourceCalendar: PersonalMoodleCalendarLive, Subject: { } subject })
@@ -34,13 +34,13 @@
3434
}
3535
<li>
3636
<md-icon>event</md-icon>
37-
@RenderDateDiff(originalEvent.Start, modifiedEvent.Start)
37+
@RenderDateDiff(EditContext.EventContext)
3838
</li>
3939
@if (!string.IsNullOrWhiteSpace(originalEvent.Location) || !string.IsNullOrWhiteSpace(modifiedEvent.Location))
4040
{
4141
<li>
4242
<md-icon>location_on</md-icon>
43-
@modifiedEvent.Location
43+
@RenderStringDiff(originalEvent.Location, modifiedEvent.Location)
4444
</li>
4545
}
4646
@if (modifiedEvent.Teachers is [_, ..])
@@ -156,7 +156,7 @@
156156
: null;
157157
}
158158
<connected-button-select
159-
value="@selectedCategoryId"
159+
value="@(selectedCategoryId ?? -1)"
160160
@onchange="@(async ev =>
161161
{
162162
int categoryId = int.Parse((string)ev.Value!);
@@ -175,6 +175,100 @@
175175
ResetCallback="@(() => ResetModifications(typeof(CategoryModification)))"/>
176176
</div>
177177

178+
@{
179+
Duration? commonOriginalLength = selectedEvents
180+
.Select(e => e.OriginalEvent.End - e.OriginalEvent.Start)
181+
.Distinct()
182+
.SingleOtherwiseNull();
183+
Duration? commonModifiedLength = selectedEvents
184+
.Select(e => e.ModifiedEvent.End - e.ModifiedEvent.Start)
185+
.Distinct()
186+
.SingleOtherwiseNull();
187+
bool anyLengthModifications = selectedEvents.Any(e => e.Modifications.Any(m => m.Action is LengthModification));
188+
}
189+
@if (commonOriginalLength is { TotalMinutes: 60 + 45 or 120 + 45 } || anyLengthModifications)
190+
{
191+
<div style="margin-top: 16px">
192+
Hossz
193+
</div>
194+
<div class="option-with-reset-button">
195+
@{
196+
Duration? commonLengthModification =
197+
commonOriginalLength is { } original && commonModifiedLength is { } modified
198+
? modified - original
199+
: null;
200+
}
201+
<div>
202+
@if (commonOriginalLength is { TotalMinutes: 60 + 45 or 120 + 45 })
203+
{
204+
<connected-button-select>
205+
<connected-button-select-option
206+
selected="@(commonLengthModification is { TotalTicks: 0 })"
207+
@onclick="@(() => ResetModifications(typeof(LengthModification)))">
208+
Eredeti (15 p szünet)
209+
</connected-button-select-option>
210+
<connected-button-select-option
211+
selected="@(commonLengthModification is { TotalMinutes: -15 })"
212+
@onclick="@(() => AddModification(new LengthModification { Length = commonOriginalLength.Value - Duration.FromMinutes(15) }))">
213+
Szünet nélkül (-15 p)
214+
</connected-button-select-option>
215+
</connected-button-select>
216+
}
217+
</div>
218+
<ResetButton
219+
IsVisible="@(anyLengthModifications)"
220+
ResetCallback="@(() => ResetModifications(typeof(LengthModification)))"/>
221+
</div>
222+
}
223+
224+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); margin-top: 16px; gap: 8px">
225+
@{
226+
string? commonTitle = selectedEvents
227+
.Select(e => e.ModifiedEvent.Title)
228+
.Distinct()
229+
.SingleOtherwiseNullClass();
230+
bool anyTitleModifications = selectedEvents.Any(e => e.Modifications.Any(m => m.Action is TitleModification));
231+
}
232+
@if (commonTitle is { } || anyTitleModifications)
233+
{
234+
<MdOutlinedTextField label="Cím"
235+
Value="@commonTitle"
236+
ValueChanged="@((string? newTitle) =>
237+
{
238+
if (newTitle == null)
239+
return Task.CompletedTask;
240+
return AddModification(new TitleModification { NewTitle = newTitle });
241+
})">
242+
<ResetButton slot="trailing-icon"
243+
IsVisible="@(anyTitleModifications)"
244+
ResetCallback="@(() => ResetModifications(typeof(TitleModification)))"/>
245+
</MdOutlinedTextField>
246+
}
247+
248+
@{
249+
string? commonLocation = selectedEvents
250+
.Select(e => e.ModifiedEvent.Location)
251+
.Distinct()
252+
.SingleOtherwiseNullClass();
253+
bool anyLocationModifications = selectedEvents.Any(e => e.Modifications.Any(m => m.Action is LocationModification));
254+
}
255+
@if (commonLocation is { } || anyLocationModifications)
256+
{
257+
<MdOutlinedTextField
258+
label="Hely"
259+
Value="@commonLocation"
260+
ValueChanged="@((string? newLocation) =>
261+
{
262+
if (newLocation == null)
263+
return Task.CompletedTask;
264+
return AddModification(new LocationModification { NewLocation = newLocation });
265+
})">
266+
<ResetButton slot="trailing-icon"
267+
IsVisible="@(anyLocationModifications)"
268+
ResetCallback="@(() => ResetModifications(typeof(LocationModification)))"/>
269+
</MdOutlinedTextField>
270+
}
271+
</div>
178272
</div>
179273
</div>
180274

@@ -244,23 +338,19 @@
244338
await CalendarUpdatedCallback.InvokeAsync();
245339
}
246340

247-
private static RenderFragment RenderDateDiff(Instant original, Instant modified)
341+
private static RenderFragment RenderStringDiff(string original, string modified)
342+
{
343+
if (original == modified || string.IsNullOrWhiteSpace(original))
344+
return @<text>@modified</text>;
345+
return @<text><del>@original</del> @modified</text>;
346+
}
347+
348+
private static RenderFragment RenderDateDiff(EventContext eventContext)
248349
{
249-
var ogZonedDateTime = original.InZone(SharedUtils.HungarianTimeZone);
250-
var modZonedDateTime = modified.InZone(SharedUtils.HungarianTimeZone);
251-
var ogDate = ogZonedDateTime.Date;
252-
var ogTime = ogZonedDateTime.TimeOfDay;
253-
var modDate = modZonedDateTime.Date;
254-
var modTime = modZonedDateTime.TimeOfDay;
255-
bool datesEqual = ogDate == modDate;
256-
bool timesEqual = ogTime == modTime;
257-
if (datesEqual && timesEqual)
258-
return @<text>@Format(modDate), @Format(modTime)</text>;
259-
if (!datesEqual && !timesEqual)
260-
return @<text><sub>@Format(ogDate), @Format(ogTime)</sub> @Format(modDate), @Format(modTime)</text>;
261-
if (!datesEqual)
262-
return @<text><sub>@Format(ogDate)</sub> @Format(modDate), @Format(modTime)</text>;
263-
return @<text>@Format(ogDate), <sub>@Format(ogTime)</sub> @Format(modTime)</text>;
350+
Instant currentInstant = SystemClock.Instance.GetCurrentInstant();
351+
string original = DateFormatter.Format(eventContext.OriginalEvent.Start, eventContext.OriginalEvent.End, currentInstant);
352+
string modified = DateFormatter.Format(eventContext.ModifiedEvent.Start, eventContext.ModifiedEvent.End, currentInstant);
353+
return RenderStringDiff(original, modified);
264354
}
265355

266356
private static string Format(LocalDate localDate)
Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,8 @@
1-
@*
2-
Wrapper around md-filled-text-field that fixes updates to the value
3-
from Blazor not being reflected.
4-
Usage:
5-
@bind-Value="@myValue"
6-
7-
https://stackoverflow.com/questions/77583025/how-to-use-bind-not-bind-property-or-bind-on-dom-property-for-custom-element
8-
*@
9-
10-
@inject IJSRuntime Js
1+
@inherits MdTextField
112

123
<md-filled-text-field
13-
@ref="_elementRef"
4+
@ref="@_elementRef"
145
@onchange="@OnChangeHandler"
15-
@attributes="AdditionalAttributes"
16-
/>
17-
18-
@code {
19-
private ElementReference _elementRef;
20-
private string? _lastValue;
21-
22-
[Parameter] public string? Value { get; set; }
23-
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
24-
[Parameter] public EventCallback<ChangeEventArgs> OnChange { get; set; }
25-
26-
[Parameter(CaptureUnmatchedValues = true)]
27-
public Dictionary<string, object>? AdditionalAttributes { get; set; }
28-
29-
protected override async Task OnAfterRenderAsync(bool firstRender)
30-
{
31-
if (Value != _lastValue)
32-
{
33-
await Js.InvokeVoidAsync("setElementProperty", _elementRef, "value", Value ?? "");
34-
_lastValue = Value;
35-
}
36-
}
37-
38-
private async Task OnChangeHandler(ChangeEventArgs e)
39-
{
40-
var newValue = (string?)e.Value;
41-
_lastValue = newValue;
42-
await ValueChanged.InvokeAsync(newValue);
43-
await OnChange.InvokeAsync(e);
44-
}
45-
}
6+
@attributes="@AdditionalAttributes">
7+
@ChildContent
8+
</md-filled-text-field>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@inherits MdTextField
2+
3+
<md-outlined-text-field
4+
@ref="@_elementRef"
5+
@onchange="@OnChangeHandler"
6+
@attributes="@AdditionalAttributes">
7+
@ChildContent
8+
</md-outlined-text-field>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.JSInterop;
3+
4+
namespace StartSch.Wasm.Components;
5+
6+
/// Wrapper around md-filled-text-field that fixes updates to the value
7+
/// from Blazor not being reflected.
8+
///
9+
/// Usage:
10+
/// @bind-Value="@myValue"
11+
///
12+
/// https://stackoverflow.com/questions/77583025/how-to-use-bind-not-bind-property-or-bind-on-dom-property-for-custom-element
13+
public class MdTextField : ComponentBase
14+
{
15+
protected ElementReference _elementRef;
16+
private string? _lastValue;
17+
18+
[Inject] private IJSRuntime Js { get; set; } = null!;
19+
20+
[Parameter] public string? Value { get; set; }
21+
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
22+
[Parameter] public EventCallback<ChangeEventArgs> OnChange { get; set; }
23+
[Parameter] public RenderFragment? ChildContent { get; set; }
24+
25+
[Parameter(CaptureUnmatchedValues = true)]
26+
public Dictionary<string, object>? AdditionalAttributes { get; set; }
27+
28+
protected override async Task OnAfterRenderAsync(bool firstRender)
29+
{
30+
if (Value != _lastValue)
31+
{
32+
await Js.InvokeVoidAsync("setElementProperty", _elementRef, "value", Value ?? "");
33+
_lastValue = Value;
34+
}
35+
}
36+
37+
protected async Task OnChangeHandler(ChangeEventArgs e)
38+
{
39+
var newValue = (string?)e.Value;
40+
_lastValue = newValue;
41+
await ValueChanged.InvokeAsync(newValue);
42+
await OnChange.InvokeAsync(e);
43+
}
44+
}

StartSch.Wasm/Components/PersonalCalendarEventDescription.razor

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
@*
1010
Google Calendar uses white-space: pre so we have to make sure not to have any useless whitespace.
11-
Whitespace seems to get removed around C# expressions.
12-
https://learn.microsoft.com/en-us/aspnet/core/blazor/components#whitespace-rendering-behavior
11+
Blazor seems to remove whitespace only before and after C# expressions: https://learn.microsoft.com/en-us/aspnet/core/blazor/components#whitespace-rendering-behavior
1312
*@
1413

1514
@if (modifiedEvent is { SourceCalendar: PersonalMoodleCalendarLive, Subject: { } })
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@if (!IsVisible) return;
22

3-
<md-icon-button @onclick="@ResetCallback" class="reset-button">
3+
<md-icon-button @onclick="@ResetCallback" class="reset-button"
4+
@attributes="AdditionalAttributes">
45
<md-icon>
56
reset_wrench
67
</md-icon>
@@ -9,4 +10,8 @@
910
@code {
1011
[Parameter, EditorRequired] public bool IsVisible { get; set; }
1112
[Parameter, EditorRequired] public EventCallback ResetCallback { get; set; }
13+
14+
[Parameter(CaptureUnmatchedValues = true)]
15+
public Dictionary<string, object>? AdditionalAttributes { get; set; }
16+
1217
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System.Text;
22
using NodaTime.Extensions;
33

4-
namespace StartSch;
4+
namespace StartSch.Wasm;
55

66
/// Outputs time in Hungary, formatted according to Hungarian rules
77
//
@@ -180,8 +180,8 @@ private static DateFormat GetDateFormat(LocalDate today, LocalDate date)
180180
}
181181
}
182182

183-
var mondayOfDate = Utils.GetMondayOfWeekOf(date);
184-
var mondayOfThisWeek = Utils.GetMondayOfWeekOf(today);
183+
var mondayOfDate = SharedUtils.GetMondayOfWeekOf(date);
184+
var mondayOfThisWeek = SharedUtils.GetMondayOfWeekOf(today);
185185
if (mondayOfThisWeek == mondayOfDate)
186186
return DateFormat.ThisWeek;
187187
if (mondayOfThisWeek.PlusWeeks(1) == mondayOfDate)
@@ -206,8 +206,8 @@ private static DateFormat GetEndDateFormat(LocalDate from, LocalDate to, LocalDa
206206
}
207207
}
208208

209-
var mondayOfDate = Utils.GetMondayOfWeekOf(to);
210-
var mondayOfThisWeek = Utils.GetMondayOfWeekOf(today);
209+
var mondayOfDate = SharedUtils.GetMondayOfWeekOf(to);
210+
var mondayOfThisWeek = SharedUtils.GetMondayOfWeekOf(today);
211211
if (mondayOfThisWeek == mondayOfDate)
212212
return DateFormat.ThisWeek;
213213
if (mondayOfThisWeek.PlusDays(7) == mondayOfDate)

StartSch.Wasm/PersonalCalendars/IModificationAction.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ namespace StartSch.Wasm.PersonalCalendars;
44

55
[JsonDerivedType(typeof(CategoryModification), nameof(CategoryModification))]
66
[JsonDerivedType(typeof(StartModification), nameof(StartModification))]
7+
[JsonDerivedType(typeof(LengthModification), nameof(LengthModification))]
8+
[JsonDerivedType(typeof(TitleModification), nameof(TitleModification))]
9+
[JsonDerivedType(typeof(LocationModification), nameof(LocationModification))]
710
public interface IModificationAction
811
{
912
void Apply(PersonalCalendarEvent target);
@@ -23,3 +26,23 @@ public class StartModification : IModificationAction
2326
public void Apply(PersonalCalendarEvent target) => target.Start += Offset;
2427
}
2528

29+
public class LengthModification : IModificationAction
30+
{
31+
public required Duration Length { get; init; }
32+
33+
public void Apply(PersonalCalendarEvent target) => target.End = target.Start + Length;
34+
}
35+
36+
public class TitleModification : IModificationAction
37+
{
38+
public required string NewTitle { get; init; }
39+
40+
public void Apply(PersonalCalendarEvent target) => target.Title = NewTitle;
41+
}
42+
43+
public class LocationModification : IModificationAction
44+
{
45+
public required string NewLocation { get; init; }
46+
47+
public void Apply(PersonalCalendarEvent target) => target.Location = NewLocation;
48+
}

0 commit comments

Comments
 (0)