Yuyuqt commited on
Commit
313d29f
·
1 Parent(s): 9edb017

add: real time update notification badge count

Browse files
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/[controller]")]
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 (_unreadCount > 0)
54
  {
55
- <span class="absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center bg-primary text-white text-[8px] font-black rounded-full border-2 border-card animate-bounce">
56
- @(_unreadCount > 9 ? "9+" : _unreadCount)
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
- protected override async Task OnAfterRenderAsync(bool firstRender)
166
  {
167
- if (firstRender)
 
 
 
 
 
168
  {
169
  var authState = await AuthStateProvider.GetAuthenticationStateAsync();
170
- var user = authState.User;
171
-
172
- if (user.Identity?.IsAuthenticated == true)
173
  {
174
- try
 
 
 
 
 
175
  {
176
- _unreadCount = await ApiClient.GetUnreadNotificationCountAsync();
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 class="group bg-card rounded-2xl p-6 border border-border hover:border-primary/30 transition-all duration-300 shadow-sm relative overflow-hidden @(!notif.IsRead ? "bg-primary/[0.02]" : "")">
 
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) await LoadNotifications();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }