Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
313d29f
1
Parent(s): 9edb017
add: real time update notification badge count
Browse files- Backend/Features/Notification/INotificationService.cs +1 -0
- Backend/Features/Notification/NotificationService.cs +10 -0
- Backend/Features/Notification/NotificationsController.cs +9 -1
- BlazorWebAssembly/Layout/MainLayout.razor +41 -25
- BlazorWebAssembly/Layout/NavMenu.razor +0 -5
- BlazorWebAssembly/Pages/Notifications.razor +28 -2
- BlazorWebAssembly/Program.cs +3 -0
- BlazorWebAssembly/Services/LibraryApiClient.cs +6 -0
- BlazorWebAssembly/Services/NotificationStateService.cs +27 -0
Backend/Features/Notification/INotificationService.cs
CHANGED
|
@@ -10,6 +10,7 @@ public interface INotificationService
|
|
| 10 |
Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null);
|
| 11 |
Task<List<NotificationDto>> GetUserNotificationsAsync(Guid userId);
|
| 12 |
Task<bool> MarkAllAsReadAsync(Guid userId);
|
|
|
|
| 13 |
Task<int> GetUnreadCountAsync(Guid userId);
|
| 14 |
}
|
| 15 |
|
|
|
|
| 10 |
Task<bool> SendAndSaveNotificationAsync(Guid userId, string? token, string title, string body, string type = "Info", string? actionLink = null, string? actionText = null);
|
| 11 |
Task<List<NotificationDto>> GetUserNotificationsAsync(Guid userId);
|
| 12 |
Task<bool> MarkAllAsReadAsync(Guid userId);
|
| 13 |
+
Task<bool> MarkAsReadAsync(int id);
|
| 14 |
Task<int> GetUnreadCountAsync(Guid userId);
|
| 15 |
}
|
| 16 |
|
Backend/Features/Notification/NotificationService.cs
CHANGED
|
@@ -123,6 +123,16 @@ public class NotificationService : INotificationService
|
|
| 123 |
return true;
|
| 124 |
}
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
public async Task<int> GetUnreadCountAsync(Guid userId)
|
| 127 |
{
|
| 128 |
return await _context.Notifications
|
|
|
|
| 123 |
return true;
|
| 124 |
}
|
| 125 |
|
| 126 |
+
public async Task<bool> MarkAsReadAsync(int id)
|
| 127 |
+
{
|
| 128 |
+
var notif = await _context.Notifications.FindAsync(id);
|
| 129 |
+
if (notif == null) return false;
|
| 130 |
+
|
| 131 |
+
notif.IsRead = true;
|
| 132 |
+
await _context.SaveChangesAsync();
|
| 133 |
+
return true;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
public async Task<int> GetUnreadCountAsync(Guid userId)
|
| 137 |
{
|
| 138 |
return await _context.Notifications
|
Backend/Features/Notification/NotificationsController.cs
CHANGED
|
@@ -9,7 +9,7 @@ using System.Threading.Tasks;
|
|
| 9 |
namespace Backend.Features.Notification;
|
| 10 |
|
| 11 |
[ApiController]
|
| 12 |
-
[Route("api/
|
| 13 |
public class NotificationsController : ControllerBase
|
| 14 |
{
|
| 15 |
private readonly INotificationService _notificationService;
|
|
@@ -58,6 +58,14 @@ public class NotificationsController : ControllerBase
|
|
| 58 |
return Ok();
|
| 59 |
}
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
[HttpPost("subscription-expiry")]
|
| 62 |
public async Task<IActionResult> SendSubscriptionExpiryNotification([FromBody] SubscriptionExpiryNotificationRequest request)
|
| 63 |
{
|
|
|
|
| 9 |
namespace Backend.Features.Notification;
|
| 10 |
|
| 11 |
[ApiController]
|
| 12 |
+
[Route("api/notifications")]
|
| 13 |
public class NotificationsController : ControllerBase
|
| 14 |
{
|
| 15 |
private readonly INotificationService _notificationService;
|
|
|
|
| 58 |
return Ok();
|
| 59 |
}
|
| 60 |
|
| 61 |
+
[HttpPost("mark-read/{id}")]
|
| 62 |
+
public async Task<IActionResult> MarkAsRead(int id)
|
| 63 |
+
{
|
| 64 |
+
var success = await _notificationService.MarkAsReadAsync(id);
|
| 65 |
+
if (!success) return NotFound();
|
| 66 |
+
return Ok();
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
[HttpPost("subscription-expiry")]
|
| 70 |
public async Task<IActionResult> SendSubscriptionExpiryNotification([FromBody] SubscriptionExpiryNotificationRequest request)
|
| 71 |
{
|
BlazorWebAssembly/Layout/MainLayout.razor
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
@inherits LayoutComponentBase
|
|
|
|
| 2 |
@using BlazorWebAssembly.Services
|
| 3 |
-
@using Microsoft.AspNetCore.Components.Authorization
|
| 4 |
@inject ThemeService ThemeService
|
| 5 |
@inject AuthenticationStateProvider AuthStateProvider
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
@inject LibraryApiClient ApiClient
|
|
|
|
| 8 |
|
| 9 |
<AuthorizeView>
|
| 10 |
<Authorized>
|
|
@@ -48,12 +49,12 @@
|
|
| 48 |
</div>
|
| 49 |
</div>
|
| 50 |
<div class="flex items-center gap-6">
|
| 51 |
-
<NavLink href="/notifications" class="relative text-muted hover:text-primary transition-colors">
|
| 52 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
| 53 |
-
@if (
|
| 54 |
{
|
| 55 |
-
<span class="absolute -top-
|
| 56 |
-
@(
|
| 57 |
</span>
|
| 58 |
}
|
| 59 |
</NavLink>
|
|
@@ -155,38 +156,53 @@
|
|
| 155 |
</AuthorizeView>
|
| 156 |
|
| 157 |
@code {
|
| 158 |
-
private int _unreadCount = 0;
|
| 159 |
-
|
| 160 |
protected override async Task OnInitializedAsync()
|
| 161 |
{
|
| 162 |
await ThemeService.InitializeAsync();
|
|
|
|
|
|
|
|
|
|
| 163 |
}
|
| 164 |
|
| 165 |
-
|
| 166 |
{
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
{
|
| 169 |
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
if (user.Identity?.IsAuthenticated == true)
|
| 173 |
{
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
{
|
| 176 |
-
|
| 177 |
-
StateHasChanged();
|
| 178 |
-
|
| 179 |
-
var token = await JS.InvokeAsync<string>("getFcmToken");
|
| 180 |
-
if (!string.IsNullOrEmpty(token))
|
| 181 |
-
{
|
| 182 |
-
await ApiClient.UpdateFcmTokenAsync(token);
|
| 183 |
-
}
|
| 184 |
-
}
|
| 185 |
-
catch (Exception ex)
|
| 186 |
-
{
|
| 187 |
-
Console.WriteLine($"FCM Token Error: {ex.Message}");
|
| 188 |
}
|
| 189 |
}
|
| 190 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
}
|
|
|
|
| 1 |
@inherits LayoutComponentBase
|
| 2 |
+
@implements IDisposable
|
| 3 |
@using BlazorWebAssembly.Services
|
|
|
|
| 4 |
@inject ThemeService ThemeService
|
| 5 |
@inject AuthenticationStateProvider AuthStateProvider
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
@inject LibraryApiClient ApiClient
|
| 8 |
+
@inject NotificationStateService NotifService
|
| 9 |
|
| 10 |
<AuthorizeView>
|
| 11 |
<Authorized>
|
|
|
|
| 49 |
</div>
|
| 50 |
</div>
|
| 51 |
<div class="flex items-center gap-6">
|
| 52 |
+
<NavLink href="/notifications" class="relative text-muted hover:text-primary transition-colors p-2 rounded-xl hover:bg-primary/5">
|
| 53 |
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
| 54 |
+
@if (NotifService.UnreadCount > 0)
|
| 55 |
{
|
| 56 |
+
<span class="absolute -top-0.5 -right-0.5 flex h-5 w-5 items-center justify-center bg-primary text-white text-[9px] font-black rounded-full border-2 border-card shadow-md animate-bounce z-10">
|
| 57 |
+
@(NotifService.UnreadCount > 9 ? "9+" : NotifService.UnreadCount)
|
| 58 |
</span>
|
| 59 |
}
|
| 60 |
</NavLink>
|
|
|
|
| 156 |
</AuthorizeView>
|
| 157 |
|
| 158 |
@code {
|
|
|
|
|
|
|
| 159 |
protected override async Task OnInitializedAsync()
|
| 160 |
{
|
| 161 |
await ThemeService.InitializeAsync();
|
| 162 |
+
AuthStateProvider.AuthenticationStateChanged += OnAuthStateChanged;
|
| 163 |
+
NotifService.OnChange += HandleNotifChange;
|
| 164 |
+
_ = UpdateNotificationCount();
|
| 165 |
}
|
| 166 |
|
| 167 |
+
private async void OnAuthStateChanged(Task<AuthenticationState> task)
|
| 168 |
{
|
| 169 |
+
await InvokeAsync(UpdateNotificationCount);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
private async Task UpdateNotificationCount()
|
| 173 |
+
{
|
| 174 |
+
try
|
| 175 |
{
|
| 176 |
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
| 177 |
+
if (authState.User.Identity?.IsAuthenticated == true)
|
|
|
|
|
|
|
| 178 |
{
|
| 179 |
+
var count = await ApiClient.GetUnreadNotificationCountAsync();
|
| 180 |
+
NotifService.SetUnreadCount(count);
|
| 181 |
+
|
| 182 |
+
// Also update FCM token if not already done
|
| 183 |
+
var token = await JS.InvokeAsync<string>("getFcmToken");
|
| 184 |
+
if (!string.IsNullOrEmpty(token))
|
| 185 |
{
|
| 186 |
+
await ApiClient.UpdateFcmTokenAsync(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
}
|
| 188 |
}
|
| 189 |
}
|
| 190 |
+
catch (Exception ex)
|
| 191 |
+
{
|
| 192 |
+
Console.WriteLine($"[MainLayout] Error updating notifications: {ex.Message}");
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
protected override void OnAfterRender(bool firstRender)
|
| 197 |
+
{
|
| 198 |
+
// No longer using OnAfterRender for initial fetch as it's handled in OnInitialized and events
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
private async void HandleNotifChange() => await InvokeAsync(StateHasChanged);
|
| 202 |
+
|
| 203 |
+
public void Dispose()
|
| 204 |
+
{
|
| 205 |
+
AuthStateProvider.AuthenticationStateChanged -= OnAuthStateChanged;
|
| 206 |
+
NotifService.OnChange -= HandleNotifChange;
|
| 207 |
}
|
| 208 |
}
|
BlazorWebAssembly/Layout/NavMenu.razor
CHANGED
|
@@ -9,11 +9,6 @@
|
|
| 9 |
Books Catalog
|
| 10 |
</NavLink>
|
| 11 |
|
| 12 |
-
<NavLink class="nav-link-v" href="/notifications">
|
| 13 |
-
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
| 14 |
-
Notifications
|
| 15 |
-
</NavLink>
|
| 16 |
-
|
| 17 |
<AuthorizeView Roles="Member">
|
| 18 |
<NavLink class="nav-link-v" href="/borrowings">
|
| 19 |
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path></svg>
|
|
|
|
| 9 |
Books Catalog
|
| 10 |
</NavLink>
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
<AuthorizeView Roles="Member">
|
| 13 |
<NavLink class="nav-link-v" href="/borrowings">
|
| 14 |
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path></svg>
|
BlazorWebAssembly/Pages/Notifications.razor
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
@using LibraryManagement.Shared.Models
|
| 3 |
@using BlazorWebAssembly.Services
|
| 4 |
@inject LibraryApiClient ApiClient
|
|
|
|
| 5 |
@inject IJSRuntime JS
|
| 6 |
|
| 7 |
<PageTitle>Notifications | LibraryLuxe</PageTitle>
|
|
@@ -50,7 +51,8 @@
|
|
| 50 |
<div class="grid gap-4">
|
| 51 |
@foreach (var notif in _notifications)
|
| 52 |
{
|
| 53 |
-
<div
|
|
|
|
| 54 |
@if (!notif.IsRead)
|
| 55 |
{
|
| 56 |
<div class="absolute left-0 top-0 bottom-0 w-1.5 bg-primary"></div>
|
|
@@ -129,7 +131,31 @@
|
|
| 129 |
try
|
| 130 |
{
|
| 131 |
var success = await ApiClient.MarkNotificationsReadAsync();
|
| 132 |
-
if (success)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
}
|
| 134 |
catch (Exception ex)
|
| 135 |
{
|
|
|
|
| 2 |
@using LibraryManagement.Shared.Models
|
| 3 |
@using BlazorWebAssembly.Services
|
| 4 |
@inject LibraryApiClient ApiClient
|
| 5 |
+
@inject NotificationStateService NotifService
|
| 6 |
@inject IJSRuntime JS
|
| 7 |
|
| 8 |
<PageTitle>Notifications | LibraryLuxe</PageTitle>
|
|
|
|
| 51 |
<div class="grid gap-4">
|
| 52 |
@foreach (var notif in _notifications)
|
| 53 |
{
|
| 54 |
+
<div @onclick="() => MarkAsRead(notif)"
|
| 55 |
+
class="group bg-card rounded-2xl p-6 border border-border hover:border-primary/30 transition-all duration-300 shadow-sm relative overflow-hidden cursor-pointer @(!notif.IsRead ? "bg-primary/[0.02]" : "")">
|
| 56 |
@if (!notif.IsRead)
|
| 57 |
{
|
| 58 |
<div class="absolute left-0 top-0 bottom-0 w-1.5 bg-primary"></div>
|
|
|
|
| 131 |
try
|
| 132 |
{
|
| 133 |
var success = await ApiClient.MarkNotificationsReadAsync();
|
| 134 |
+
if (success)
|
| 135 |
+
{
|
| 136 |
+
NotifService.SetUnreadCount(0);
|
| 137 |
+
await LoadNotifications();
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
catch (Exception ex)
|
| 141 |
+
{
|
| 142 |
+
Console.WriteLine($"Error: {ex.Message}");
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
private async Task MarkAsRead(NotificationDto notif)
|
| 147 |
+
{
|
| 148 |
+
if (notif.IsRead) return;
|
| 149 |
+
|
| 150 |
+
try
|
| 151 |
+
{
|
| 152 |
+
var success = await ApiClient.MarkAsReadAsync(notif.Id);
|
| 153 |
+
if (success)
|
| 154 |
+
{
|
| 155 |
+
notif.IsRead = true;
|
| 156 |
+
NotifService.DecrementCount();
|
| 157 |
+
StateHasChanged();
|
| 158 |
+
}
|
| 159 |
}
|
| 160 |
catch (Exception ex)
|
| 161 |
{
|
BlazorWebAssembly/Program.cs
CHANGED
|
@@ -35,4 +35,7 @@ builder.Services.AddScoped<LibraryApiClient>();
|
|
| 35 |
// Register WishlistService
|
| 36 |
builder.Services.AddScoped<WishlistService>();
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
await builder.Build().RunAsync();
|
|
|
|
| 35 |
// Register WishlistService
|
| 36 |
builder.Services.AddScoped<WishlistService>();
|
| 37 |
|
| 38 |
+
// Register NotificationStateService
|
| 39 |
+
builder.Services.AddScoped<NotificationStateService>();
|
| 40 |
+
|
| 41 |
await builder.Build().RunAsync();
|
BlazorWebAssembly/Services/LibraryApiClient.cs
CHANGED
|
@@ -383,6 +383,12 @@ namespace BlazorWebAssembly.Services
|
|
| 383 |
return response.IsSuccessStatusCode;
|
| 384 |
}
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
public async Task<bool> UpdateFcmTokenAsync(string fcmToken)
|
| 387 |
{
|
| 388 |
var response = await _httpClient.PostAsJsonAsync("api/users/fcm-token", new { fcmToken });
|
|
|
|
| 383 |
return response.IsSuccessStatusCode;
|
| 384 |
}
|
| 385 |
|
| 386 |
+
public async Task<bool> MarkAsReadAsync(int id)
|
| 387 |
+
{
|
| 388 |
+
var response = await _httpClient.PostAsync($"api/notifications/mark-read/{id}", null);
|
| 389 |
+
return response.IsSuccessStatusCode;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
public async Task<bool> UpdateFcmTokenAsync(string fcmToken)
|
| 393 |
{
|
| 394 |
var response = await _httpClient.PostAsJsonAsync("api/users/fcm-token", new { fcmToken });
|
BlazorWebAssembly/Services/NotificationStateService.cs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System;
|
| 2 |
+
|
| 3 |
+
namespace BlazorWebAssembly.Services
|
| 4 |
+
{
|
| 5 |
+
public class NotificationStateService
|
| 6 |
+
{
|
| 7 |
+
public int UnreadCount { get; private set; }
|
| 8 |
+
public event Action? OnChange;
|
| 9 |
+
|
| 10 |
+
public void SetUnreadCount(int count)
|
| 11 |
+
{
|
| 12 |
+
UnreadCount = count;
|
| 13 |
+
NotifyStateChanged();
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
public void DecrementCount()
|
| 17 |
+
{
|
| 18 |
+
if (UnreadCount > 0)
|
| 19 |
+
{
|
| 20 |
+
UnreadCount--;
|
| 21 |
+
NotifyStateChanged();
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
private void NotifyStateChanged() => OnChange?.Invoke();
|
| 26 |
+
}
|
| 27 |
+
}
|