User
add: language switch
8c88a89
Raw
History Blame Contribute Delete
10.8 kB
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@implements IAsyncDisposable
@inject ApiClientService ApiClient
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
@inject IJSRuntime JS
<div class="relative">
<button type="button"
@onclick="ToggleOpenAsync"
class="control-button relative">
<i data-lucide="bell" class="h-5 w-5"></i>
@if (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>
@if (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">@AppLocalization.Text("notification.notifications", "Notifications")</p>
<p class="text-xs text-slate-500">@UnreadCount @AppLocalization.Text("notification.unread", "unread")</p>
</div>
<div class="flex items-center gap-2">
@if (Notifications.Any(item => item.IsRead))
{
<button type="button" class="text-xs font-semibold text-rose-600 hover:text-rose-700" @onclick="RequestCleanupConfirmation">
@AppLocalization.Text("notification.clearRead", "Clear read")
</button>
}
<button type="button" class="text-xs font-semibold text-violet-600 hover:text-violet-700" @onclick="RefreshAsync">
@AppLocalization.Text("common.refresh", "Refresh")
</button>
<button type="button" class="text-xs font-semibold text-slate-500 hover:text-slate-700" @onclick="Close">
@AppLocalization.Text("common.close", "Close")
</button>
</div>
</div>
<div class="max-h-[26rem] overflow-y-auto">
@if (IsLoading)
{
<div class="p-4 text-sm text-slate-500">@AppLocalization.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">@AppLocalization.Text("notification.noneYet", "No notifications yet")</p>
<p class="text-xs text-slate-500 mt-1">@AppLocalization.Text("notification.emptyState", "Your task alerts will show up here.")</p>
</div>
}
else
{
@foreach (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")"
@onclick="() => 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">@item.Title</p>
@if (!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">@item.Body</p>
<div class="mt-2 flex items-center justify-between text-[11px] text-slate-400">
<span>@FormatSender(item.SenderName)</span>
<span>@FormatTime(item.CreatedAt)</span>
</div>
</div>
</div>
</button>
}
}
</div>
</div>
}
</div>
<ConfirmDialog IsVisible="showCleanupConfirmDialog"
Title='@AppLocalization.Text("notification.clearReadTitle", "Clear read notifications?")'
Message='@AppLocalization.Text("notification.clearReadMessage", "This will permanently remove all read notifications from your list.")'
ConfirmText='@AppLocalization.Text("common.clear", "Clear")'
Icon="trash-2"
OnConfirm="ConfirmCleanupAsync"
OnCancel="CloseCleanupConfirmDialog" />
@code {
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;
}