Skip to content

Commit 97a3b53

Browse files
authored
[Admin] Component: Create event details - UI component #214 (#293)
* Feat: 어드민 UI 컴포넌트 Page 추가 * Test: 어드민 UI 컴포넌트 테스트 추가 * Feat: 버튼 추가 * Fix: 피드백 적용 - AzureOpenAIProxy.sln 파일 이전 내용으로 되돌리기 - AzureOpenAIProxy.PlaygroundApp.Tests/Pages/NewEventDetailsTests.cs 테스트 코드 삭제 * Update PlaygroundApp Model 최신화 * Refactor: 컴포넌트 수정 - Id 파라미터 추가 - 버튼 Id 추가 - TextFieldType 추가 - Id 값 kebab-casing 수정 * Feat: NodaTime을 사용하여 Time Zone Option 추가 * Feat: 기본값 및 이벤트 설정 - 기본 날짜/시간 설정 추가 - 이벤트 추가/취소 버튼 이벤트 바인딩 추가 - Max Token Cap, Daily Request Cap 값 바인딩 * Refactor: 불필요한 코드 정리 * Refactor: Time Zone Select 높이 수정 * Refactor: CSS 적용 방식 수정 - 외부 스타일 시트 적용 * Revert "Refactor: CSS 적용 방식 수정 - 외부 스타일 시트 적용" This reverts commit 197d7ee. * Refactor: 이벤트 종료 날짜 기본값 수정 (오늘 기준 다음날로 적용) * Refactor: NewEventDetailsComponent.razor * Refactor: Remove @temp~ variables * Fix: FluentDatePicker, FluentTimePicker ValueChanged error fix * Refactor: NewEventDetailsComponent.razor * Test: Add NewEventDetailsComponent.razor test * Test: Add input test * Fix: delete inject * Feat: Get local browser timezone * Refactor: Add JS error handling * Refactor: NewEventDetailsComponent.razor * Test: Add init timezone test * Fix: Browser Timezone > System Timezone * Fix: Test error fix (Now > UtcNow) * Fix: add attribute Culture to FluentDatePicker * Fix: Add culture info in OnAfterRenderAsync * Feat: Convert from Windows timezone to IANA timezone using TimeZoneConverter * Test: Convert from Windows timezone to IANA timezone using TimeZoneConverter * Fix: Check OS to get timezone * Test: Refactoring NewEventDetailsPageTests * Test: Refactoring NewEventDetailsPageTests * Refactor: Refactoring NewEventDetailsComponent and test - Delete TimeZoneConverter - Split input event datetime test * Test: Add NewEventDetailsPageTests.cs to AppHost test
1 parent 83f0f3b commit 97a3b53

File tree

8 files changed

+536
-97
lines changed

8 files changed

+536
-97
lines changed

src/AzureOpenAIProxy.PlaygroundApp/AzureOpenAIProxy.PlaygroundApp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="$(FluentUIVersion)" />
1111
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Emoji" Version="$(FluentUIVersion)" />
1212
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="$(FluentUIVersion)" />
13+
<PackageReference Include="NodaTime" Version="3.1.12" />
1314
</ItemGroup>
1415

1516
<ItemGroup>

src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
<PageTitle>New event</PageTitle>
44

55
<h1>New event</h1>
6+
7+
<NewEventDetailsComponent Id="admin-new-event" @rendermode="InteractiveServer" />
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
@using AzureOpenAIProxy.PlaygroundApp.Clients
2+
@using AzureOpenAIProxy.PlaygroundApp.Models;
3+
4+
@using System.Globalization
5+
6+
@using NodaTime
7+
@using NodaTime.Extensions
8+
@using NodaTime.TimeZones
9+
10+
<FluentLayout Id="@Id">
11+
@if (adminEventDetails == null)
12+
{
13+
<p><em>Loading...</em></p>
14+
}
15+
else
16+
{
17+
<FluentHeader>New Event</FluentHeader>
18+
<FluentBodyContent>
19+
<section>
20+
<h2>Event Infomation</h2>
21+
22+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
23+
<FluentLabel For="event-title" Class="create-input-label">Title</FluentLabel>
24+
<FluentTextField Id="event-title" Name="title" TextFieldType="TextFieldType.Text" Required />
25+
</FluentStack>
26+
27+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
28+
<FluentLabel For="event-summary" Class="create-input-label">Summary</FluentLabel>
29+
<FluentTextField id="event-summary" TextFieldType="TextFieldType.Text" Required />
30+
</FluentStack>
31+
32+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
33+
<FluentLabel For="event-description" Class="create-input-label">Description</FluentLabel>
34+
<FluentTextArea Id="event-description" Style="width:300px" />
35+
</FluentStack>
36+
37+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
38+
<FluentLabel For="event-start-date" Class="create-input-label">Event Start Date</FluentLabel>
39+
<FluentDatePicker Id="event-start-date" Value="@adminEventDetails.DateStart.DateTime" ValueChanged="@(e => adminEventDetails.DateStart = e.Value)" Culture="System.Globalization.CultureInfo.CurrentCulture" />
40+
<FluentTimePicker Id="event-start-time" Value="@adminEventDetails.DateStart.DateTime" ValueChanged="@(e => adminEventDetails.DateStart = e.Value)" />
41+
</FluentStack>
42+
43+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
44+
<FluentLabel For="event-end-date" Class="create-input-label">Event End Date</FluentLabel>
45+
<FluentDatePicker Id="event-end-date" Value="@adminEventDetails.DateEnd.DateTime" ValueChanged="@(e => adminEventDetails.DateEnd = e.Value)" Culture="System.Globalization.CultureInfo.CurrentCulture" />
46+
<FluentTimePicker Id="event-end-time" Value="@adminEventDetails.DateEnd.DateTime" ValueChanged="@(e => adminEventDetails.DateEnd = e.Value)" />
47+
</FluentStack>
48+
49+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
50+
<FluentLabel For="event-timezone" Class="create-input-label">Time Zone</FluentLabel>
51+
<FluentSelect Id="event-timezone" @bind-Value="@adminEventDetails.TimeZone" Height="500px" TOption="string" Required>
52+
@foreach (var timeZone in timeZoneList)
53+
{
54+
<FluentOption Value="@timeZone.Id">@timeZone.Id</FluentOption>
55+
}
56+
</FluentSelect>
57+
</FluentStack>
58+
</section>
59+
60+
<section>
61+
<h2>Event Organizer</h2>
62+
63+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
64+
<FluentLabel For="event-organizer-name" Class="create-input-label">Organizer Name</FluentLabel>
65+
<FluentTextField Id="event-organizer-name" TextFieldType="TextFieldType.Text" Required />
66+
</FluentStack>
67+
68+
69+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
70+
<FluentLabel For="event-organizer-email" Class="create-input-label">Organizer Email</FluentLabel>
71+
<FluentTextField Id="event-organizer-email" TextFieldType="TextFieldType.Email" Required />
72+
</FluentStack>
73+
</section>
74+
75+
<section>
76+
<h2>Event Coorganizers</h2>
77+
78+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
79+
<FluentLabel For="event-coorgnizer-name" Class="create-input-label">Coorgnizer Name</FluentLabel>
80+
<FluentTextField Id="event-coorgnizer-name" TextFieldType="TextFieldType.Text" Required />
81+
</FluentStack>
82+
83+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
84+
<FluentLabel For="event-coorgnizer-email" Class="create-input-label">Coorgnizer Email</FluentLabel>
85+
<FluentTextField Id="event-coorgnizer-email" TextFieldType="TextFieldType.Email" Required />
86+
</FluentStack>
87+
</section>
88+
89+
<section>
90+
<h2>Event Configuration</h2>
91+
92+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
93+
<FluentLabel For="event-max-token-cap" Class="create-input-label">Max Token Cap</FluentLabel>
94+
<FluentNumberField Id="event-max-token-cap" @bind-Value="adminEventDetails.MaxTokenCap" Required />
95+
</FluentStack>
96+
97+
<FluentStack Class="create-fluent-stack" Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center">
98+
<FluentLabel For="event-daily-request-cap" Class="create-input-label">Daily Request Cap</FluentLabel>
99+
<FluentNumberField Id="event-daily-request-cap" @bind-Value="adminEventDetails.DailyRequestCap" Required />
100+
</FluentStack>
101+
</section>
102+
103+
<section class="button-section">
104+
<FluentButton Id="admin-event-detail-add" Appearance="Appearance.Accent" Class="button" OnClick="AddEvent">Add Event</FluentButton>
105+
<FluentButton Id="admin-event-detail-cancel" Appearance="Appearance.Outline" Class="button" OnClick="CancelEvent">Cancel</FluentButton>
106+
</section>
107+
</FluentBodyContent>
108+
}
109+
</FluentLayout>
110+
111+
112+
@code {
113+
private List<DateTimeZone>? timeZoneList;
114+
private AdminEventDetails? adminEventDetails;
115+
private DateTimeOffset currentTime = DateTimeOffset.UtcNow;
116+
117+
[Parameter]
118+
public string? Id { get; set; }
119+
120+
protected override async Task OnInitializedAsync()
121+
{
122+
adminEventDetails = adminEventDetails == null ? new() : adminEventDetails;
123+
124+
timeZoneList = DateTimeZoneProviders.Tzdb.GetAllZones().ToList();
125+
126+
CultureInfo customCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
127+
customCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-dd";
128+
customCulture.DateTimeFormat.ShortTimePattern = "HH:mm";
129+
130+
CultureInfo.DefaultThreadCurrentCulture = customCulture;
131+
CultureInfo.DefaultThreadCurrentUICulture = customCulture;
132+
}
133+
134+
protected override async Task OnAfterRenderAsync(bool firstRender)
135+
{
136+
if (firstRender)
137+
{
138+
var timezoneId = GetIanaTimezoneId();
139+
currentTime = GetCurrentDateTimeOffset(timezoneId);
140+
141+
adminEventDetails.DateStart = currentTime.AddHours(1).AddMinutes(-currentTime.Minute);
142+
adminEventDetails.DateEnd = currentTime.AddDays(1).AddHours(1).AddMinutes(-currentTime.Minute);
143+
adminEventDetails.TimeZone = timezoneId;
144+
145+
await InvokeAsync(StateHasChanged);
146+
}
147+
}
148+
149+
private async Task AddEvent()
150+
{
151+
await Task.CompletedTask;
152+
}
153+
154+
private async Task CancelEvent()
155+
{
156+
await Task.CompletedTask;
157+
}
158+
159+
private string GetIanaTimezoneId()
160+
{
161+
string timezoneId = TimeZoneInfo.Local.Id;
162+
163+
if (OperatingSystem.IsWindows())
164+
{
165+
if (TimeZoneInfo.TryConvertWindowsIdToIanaId(timezoneId, out var ianaTimezoneId))
166+
{
167+
timezoneId = ianaTimezoneId;
168+
}
169+
}
170+
171+
return timezoneId;
172+
}
173+
174+
private DateTimeOffset GetCurrentDateTimeOffset(string timezoneId)
175+
{
176+
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezoneId);
177+
178+
return TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZoneInfo);
179+
}
180+
}
181+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
section {
2+
margin-bottom: 100px
3+
}
4+
5+
::deep .create-input-label {
6+
width: 200px;
7+
--type-ramp-base-font-size: 22px;
8+
}
9+
10+
::deep .create-fluent-stack {
11+
height: 100px;
12+
}
13+
14+
.button-section {
15+
display: flex;
16+
justify-content: center;
17+
gap: 50px;
18+
}
19+
20+
.button {
21+
width: 150px;
22+
height: 50px;
23+
font-size: 16px;
24+
margin: 0 10px;
25+
}
Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,60 @@
1-
using System.Text.Json.Serialization;
2-
3-
namespace AzureOpenAIProxy.PlaygroundApp.Models;
4-
5-
/// <summary>
6-
/// This represent the event detail data for response by admin event endpoint.
7-
/// </summary>
8-
public class AdminEventDetails : EventDetails
9-
{
10-
/// <summary>
11-
/// Gets or sets the event description.
12-
/// </summary>
13-
public string? Description { get; set; }
14-
15-
/// <summary>
16-
/// Gets or sets the event start date.
17-
/// </summary>
18-
[JsonRequired]
19-
public DateTimeOffset DateStart { get; set; }
20-
21-
/// <summary>
22-
/// Gets or sets the event end date.
23-
/// </summary>
24-
[JsonRequired]
25-
public DateTimeOffset DateEnd { get; set; }
26-
27-
/// <summary>
28-
/// Gets or sets the event start to end date timezone.
29-
/// </summary>
30-
[JsonRequired]
31-
public string TimeZone { get; set; } = string.Empty;
32-
33-
/// <summary>
34-
/// Gets or sets the event active status.
35-
/// </summary>
36-
[JsonRequired]
37-
public bool IsActive { get; set; }
38-
39-
/// <summary>
40-
/// Gets or sets the event organizer name.
41-
/// </summary>
42-
[JsonRequired]
43-
public string OrganizerName { get; set; } = string.Empty;
44-
45-
/// <summary>
46-
/// Gets or sets the event organizer email.
47-
/// </summary>
48-
[JsonRequired]
49-
public string OrganizerEmail { get; set; } = string.Empty;
50-
51-
/// <summary>
52-
/// Gets or sets the event coorganizer name.
53-
/// </summary>
54-
public string? CoorganizerName { get; set; }
55-
56-
/// <summary>
57-
/// Gets or sets the event coorganizer email.
58-
/// </summary>
59-
public string? CoorganizerEmail { get; set; }
1+
using System.Text.Json.Serialization;
2+
3+
namespace AzureOpenAIProxy.PlaygroundApp.Models;
4+
5+
/// <summary>
6+
/// This represent the event detail data for response by admin event endpoint.
7+
/// </summary>
8+
public class AdminEventDetails : EventDetails
9+
{
10+
/// <summary>
11+
/// Gets or sets the event description.
12+
/// </summary>
13+
public string? Description { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets the event start date.
17+
/// </summary>
18+
[JsonRequired]
19+
public DateTimeOffset DateStart { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the event end date.
23+
/// </summary>
24+
[JsonRequired]
25+
public DateTimeOffset DateEnd { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the event start to end date timezone.
29+
/// </summary>
30+
[JsonRequired]
31+
public string TimeZone { get; set; } = string.Empty;
32+
33+
/// <summary>
34+
/// Gets or sets the event active status.
35+
/// </summary>
36+
[JsonRequired]
37+
public bool IsActive { get; set; }
38+
39+
/// <summary>
40+
/// Gets or sets the event organizer name.
41+
/// </summary>
42+
[JsonRequired]
43+
public string OrganizerName { get; set; } = string.Empty;
44+
45+
/// <summary>
46+
/// Gets or sets the event organizer email.
47+
/// </summary>
48+
[JsonRequired]
49+
public string OrganizerEmail { get; set; } = string.Empty;
50+
51+
/// <summary>
52+
/// Gets or sets the event coorganizer name.
53+
/// </summary>
54+
public string? CoorganizerName { get; set; }
55+
56+
/// <summary>
57+
/// Gets or sets the event coorganizer email.
58+
/// </summary>
59+
public string? CoorganizerEmail { get; set; }
6060
}

0 commit comments

Comments
 (0)