Spaces:
Running
Running
| @(new InteractiveServerRenderMode(prerender: false)) | |
| IAsyncDisposable | |
| ApiClientService ApiClient | |
| AuthenticationStateProvider AuthStateProvider | |
| NavigationManager Navigation | |
| IJSRuntime JS | |
| <div class="relative"> | |
| <button type="button" | |
| ="ToggleOpenAsync" | |
| class="control-button relative"> | |
| <i data-lucide="bell" class="h-5 w-5"></i> | |
| (UnreadCount > 0) | |
| { | |
| <span class="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-rose-600 px-1 text-[10px] font-bold text-white"> | |
| @(UnreadCount > 99 ? "99+" : UnreadCount.ToString()) | |
| </span> | |
| } | |
| </button> | |
| (IsOpen) | |
| { | |
| <div class="absolute right-0 z-50 mt-3 w-[380px] max-w-[calc(100vw-2rem)] overflow-hidden rounded-lg border border-slate-200 bg-white shadow-lg"> | |
| <div class="flex items-center justify-between px-4 py-3 border-b border-slate-100 bg-slate-50/80"> | |
| <div> | |
| <p class="text-sm font-semibold text-slate-900">.Text("notification.notifications", "Notifications")</p> | |
| <p class="text-xs text-slate-500"> .Text("notification.unread", "unread")</p> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| (Notifications.Any(item => item.IsRead)) | |
| { | |
| <button type="button" class="text-xs font-semibold text-rose-600 hover:text-rose-700" ="RequestCleanupConfirmation"> | |
| .Text("notification.clearRead", "Clear read") | |
| </button> | |
| } | |
| <button type="button" class="text-xs font-semibold text-violet-600 hover:text-violet-700" ="RefreshAsync"> | |
| .Text("common.refresh", "Refresh") | |
| </button> | |
| <button type="button" class="text-xs font-semibold text-slate-500 hover:text-slate-700" ="Close"> | |
| .Text("common.close", "Close") | |
| </button> | |
| </div> | |
| </div> | |
| <div class="max-h-[26rem] overflow-y-auto"> | |
| (IsLoading) | |
| { | |
| <div class="p-4 text-sm text-slate-500">.Text("notification.loading", "Loading notifications...")</div> | |
| } | |
| else if (Notifications.Count == 0) | |
| { | |
| <div class="p-6 text-center"> | |
| <div class="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-100 text-slate-400"> | |
| <i data-lucide="inbox" class="h-5 w-5"></i> | |
| </div> | |
| <p class="text-sm font-medium text-slate-700">.Text("notification.noneYet", "No notifications yet")</p> | |
| <p class="text-xs text-slate-500 mt-1">.Text("notification.emptyState", "Your task alerts will show up here.")</p> | |
| </div> | |
| } | |
| else | |
| { | |
| (var item in Notifications) | |
| { | |
| <button type="button" | |
| class="w-full border-b border-slate-100 px-4 py-3 text-left transition-colors hover:bg-slate-50 @(item.IsRead ? "bg-white" : "bg-violet-50/70")" | |
| ="() => OpenNotificationAsync(item)"> | |
| <div class="flex items-start gap-3"> | |
| <div class="mt-0.5 flex h-9 w-9 items-center justify-center rounded-full @(item.IsRead ? "bg-slate-100 text-slate-500" : "bg-violet-100 text-violet-700")"> | |
| <i data-lucide="bell-ring" class="h-4 w-4"></i> | |
| </div> | |
| <div class="min-w-0 flex-1"> | |
| <div class="flex items-start justify-between gap-3"> | |
| <p class="text-sm font-semibold text-slate-900 truncate">.Title</p> | |
| (!item.IsRead) | |
| { | |
| <span class="mt-1 h-2.5 w-2.5 rounded-full bg-violet-600 shrink-0"></span> | |
| } | |
| </div> | |
| <p class="mt-1 text-xs text-slate-600 leading-5">.Body</p> | |
| <div class="mt-2 flex items-center justify-between text-[11px] text-slate-400"> | |
| <span>(item.SenderName)</span> | |
| <span>(item.CreatedAt)</span> | |
| </div> | |
| </div> | |
| </div> | |
| </button> | |
| } | |
| } | |
| </div> | |
| </div> | |
| } | |
| </div> | |
| <ConfirmDialog IsVisible="showCleanupConfirmDialog" | |
| Title='.Text("notification.clearReadTitle", "Clear read notifications?")' | |
| Message='.Text("notification.clearReadMessage", "This will permanently remove all read notifications from your list.")' | |
| ConfirmText='.Text("common.clear", "Clear")' | |
| Icon="trash-2" | |
| OnConfirm="ConfirmCleanupAsync" | |
| OnCancel="CloseCleanupConfirmDialog" /> | |
| { | |
| private bool IsOpen; | |
| private bool IsLoading; | |
| private bool showCleanupConfirmDialog; | |
| private int UnreadCount; | |
| private List<NotificationDto> Notifications = new(); | |
| protected override async Task OnInitializedAsync() | |
| { | |
| await RefreshAsync(); | |
| } | |
| protected override async Task OnAfterRenderAsync(bool firstRender) | |
| { | |
| await JS.InvokeVoidAsync("initIcons"); | |
| } | |
| private async Task ToggleOpenAsync() | |
| { | |
| IsOpen = !IsOpen; | |
| if (IsOpen) | |
| { | |
| await RefreshAsync(); | |
| } | |
| } | |
| private void Close() | |
| { | |
| IsOpen = false; | |
| } | |
| private async Task RefreshAsync() | |
| { | |
| IsLoading = true; | |
| try | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| if (authState.User.Identity?.IsAuthenticated != true) | |
| { | |
| Notifications.Clear(); | |
| UnreadCount = 0; | |
| return; | |
| } | |
| var client = ApiClient.CreateClient(authState.User); | |
| var unreadTask = client.GetFromJsonAsync<int>("Notification/unread-count"); | |
| var itemsTask = client.GetFromJsonAsync<List<NotificationDto>>("Notification/mine?take=10"); | |
| await Task.WhenAll(unreadTask, itemsTask); | |
| UnreadCount = unreadTask.Result; | |
| Notifications = (itemsTask.Result ?? new List<NotificationDto>()) | |
| .Select(item => | |
| { | |
| item.TargetUrl = BuildTargetUrl(item); | |
| return item; | |
| }) | |
| .ToList(); | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[DEBUG NotificationBell] Failed to refresh: {ex.Message}"); | |
| } | |
| finally | |
| { | |
| IsLoading = false; | |
| await InvokeAsync(StateHasChanged); | |
| } | |
| } | |
| private void RequestCleanupConfirmation() | |
| { | |
| showCleanupConfirmDialog = true; | |
| } | |
| private void CloseCleanupConfirmDialog() | |
| { | |
| showCleanupConfirmDialog = false; | |
| } | |
| private async Task ConfirmCleanupAsync() | |
| { | |
| showCleanupConfirmDialog = false; | |
| await ClearReadNotificationsAsync(); | |
| } | |
| private async Task ClearReadNotificationsAsync() | |
| { | |
| try | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| if (authState.User.Identity?.IsAuthenticated != true) | |
| { | |
| return; | |
| } | |
| var client = ApiClient.CreateClient(authState.User); | |
| var response = await client.DeleteAsync("Notification/read"); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| await RefreshAsync(); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[DEBUG NotificationBell] Clear read notifications failed: {ex.Message}"); | |
| } | |
| } | |
| private async Task MarkReadAsync(NotificationDto item) | |
| { | |
| if (item.IsRead) | |
| { | |
| Navigation.NavigateTo(GetTargetUrl(item)); | |
| return; | |
| } | |
| try | |
| { | |
| var authState = await AuthStateProvider.GetAuthenticationStateAsync(); | |
| if (authState.User.Identity?.IsAuthenticated != true) | |
| { | |
| return; | |
| } | |
| var client = ApiClient.CreateClient(authState.User); | |
| var response = await client.PostAsync($"Notification/{item.Id}/read", null); | |
| if (response.IsSuccessStatusCode) | |
| { | |
| item.IsRead = true; | |
| if (UnreadCount > 0) | |
| { | |
| UnreadCount--; | |
| } | |
| await InvokeAsync(StateHasChanged); | |
| Navigation.NavigateTo(GetTargetUrl(item)); | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine($"[DEBUG NotificationBell] MarkRead failed: {ex.Message}"); | |
| } | |
| } | |
| private async Task OpenNotificationAsync(NotificationDto item) | |
| { | |
| await MarkReadAsync(item); | |
| IsOpen = false; | |
| } | |
| private static string GetTargetUrl(NotificationDto item) | |
| { | |
| if (!string.IsNullOrWhiteSpace(item.TargetUrl)) | |
| { | |
| return item.TargetUrl; | |
| } | |
| return BuildTargetUrl(item); | |
| } | |
| private static string BuildTargetUrl(NotificationDto item) | |
| { | |
| return NotificationNavigation.BuildTargetUrl(item.SourceType, item.SourceId, item.NotificationType); | |
| } | |
| private static string FormatSender(string? senderName) | |
| { | |
| return string.IsNullOrWhiteSpace(senderName) ? "System" : senderName; | |
| } | |
| private static string FormatTime(DateTime? createdAt) | |
| { | |
| if (!createdAt.HasValue) | |
| { | |
| return string.Empty; | |
| } | |
| var value = createdAt.Value.ToLocalTime(); | |
| return value.ToString("dd MMM, HH:mm"); | |
| } | |
| public ValueTask DisposeAsync() => ValueTask.CompletedTask; | |
| } | |