From a60559096d7c31c1b2191cbea6f32fd1de0dbd5b Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Thu, 29 Aug 2024 20:53:20 +0530 Subject: [PATCH 01/17] added dummy notifications on top bar --- web/static/custom/custom.css | 104 +++++++++++++++++++++++++ web/templates/base/_items/top_bar.html | 57 +++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/web/static/custom/custom.css b/web/static/custom/custom.css index 52835d872..c47eebea3 100644 --- a/web/static/custom/custom.css +++ b/web/static/custom/custom.css @@ -407,3 +407,107 @@ mark{ .scan-config-modal .badge { margin-left: 5px; } + +/*.notification-a-icon { + +}*/ + +.badge-notif-count { + font-size: 10px !important; +} + +.notif-counter { + top: 9px !important; + right: 5px !important; +} + +.notification-panel-dropdown { + width: 380px; + border: none; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + padding: 0; + overflow: hidden; +} + +.notification-panel-header { + background-color: #f8f9fa; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.notification-panel-clear-link { + color: #6c757d; + text-decoration: none; + font-size: 0.875rem; + transition: color 0.2s ease; +} + +.notification-panel-clear-link:hover { + color: #495057; +} + +.notification-panel-body { + max-height: 400px; + /* overflow-y: auto; */ +} + +.notification-panel-item { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + transition: background-color 0.2s ease; +} + +.notification-panel-item:last-child { + border-bottom: none; +} + +.notification-panel-item:hover { + background-color: #f8f9fa; +} + +.notification-panel-unread { + background-color: #fafbff; + border-left: 3px solid #4a90e2; +} + +.notification-panel-unread:hover { + background-color: #f0f4ff; +} + +.notification-panel-icon { + font-size: 1.25rem; +} + +.notification-panel-title { + color: #212529; + font-size: 0.95rem; + font-weight: 600; +} + +.notification-panel-description { + color: #6c757d; + font-size: 0.875rem; + margin-bottom: 0.25rem; + line-height: 1.4; +} + +.notification-panel-time { + color: #adb5bd; + font-size: 0.75rem; +} + +.notification-panel-footer { + background-color: #f8f9fa; + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.notification-panel-view-all { + color: #007bff; + text-decoration: none; + font-size: 0.875rem; + font-weight: 600; + transition: color 0.2s ease; +} + +.notification-panel-view-all:hover { + color: #0056b3; +} \ No newline at end of file diff --git a/web/templates/base/_items/top_bar.html b/web/templates/base/_items/top_bar.html index 8b3d3c074..4ab10ad55 100644 --- a/web/templates/base/_items/top_bar.html +++ b/web/templates/base/_items/top_bar.html @@ -160,10 +160,65 @@
Welcome {{user.get_username}}!
+ From 37e5b215cfb0d0e364c5ab6a00b8461cffcc08a6 Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Fri, 30 Aug 2024 06:11:25 +0530 Subject: [PATCH 02/17] added buzzing effect on notification --- web/static/custom/custom.css | 15 +++++++++++++++ web/templates/base/_items/top_bar.html | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/web/static/custom/custom.css b/web/static/custom/custom.css index c47eebea3..227243885 100644 --- a/web/static/custom/custom.css +++ b/web/static/custom/custom.css @@ -510,4 +510,19 @@ mark{ .notification-panel-view-all:hover { color: #0056b3; +} + +@keyframes notification-buzz { + 0% { transform: translateX(0) rotate(0); } + 15% { transform: translateX(-5px) rotate(-5deg); } + 30% { transform: translateX(5px) rotate(5deg); } + 45% { transform: translateX(-4px) rotate(-4deg); } + 60% { transform: translateX(4px) rotate(4deg); } + 75% { transform: translateX(-2px) rotate(-2deg); } + 90% { transform: translateX(2px) rotate(2deg); } + 100% { transform: translateX(0) rotate(0); } +} + +.notification-bell-icon.buzz { + animation: notification-buzz 0.8s ease-out; } \ No newline at end of file diff --git a/web/templates/base/_items/top_bar.html b/web/templates/base/_items/top_bar.html index 4ab10ad55..092e752c2 100644 --- a/web/templates/base/_items/top_bar.html +++ b/web/templates/base/_items/top_bar.html @@ -162,7 +162,9 @@
Welcome {{user.get_username}}!
diff --git a/web/templates/base/base.html b/web/templates/base/base.html index b633e356f..a8eefabb8 100644 --- a/web/templates/base/base.html +++ b/web/templates/base/base.html @@ -102,6 +102,7 @@

{% block page_title %}{% endblock page_title %}

+ From 3f20709b3b60152a623d029ad5611820f4767f58 Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Fri, 30 Aug 2024 06:52:52 +0530 Subject: [PATCH 09/17] update no notification --- web/static/custom/notification.js | 32 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/web/static/custom/notification.js b/web/static/custom/notification.js index 57d062c53..32470d8f8 100644 --- a/web/static/custom/notification.js +++ b/web/static/custom/notification.js @@ -16,12 +16,21 @@ function updateNotifications() { ); notificationPanel.innerHTML = ""; - data.forEach((notification) => { - const notificationItem = document.createElement("div"); - notificationItem.className = `notification-panel-item d-flex align-items-start p-3 ${ - notification.is_read ? "" : "notification-panel-unread" - }`; - notificationItem.innerHTML = ` + if (data.length === 0) { + const noNotificationsMessage = document.createElement("div"); + noNotificationsMessage.className = + "notification-panel-item d-flex align-items-center justify-content-center p-3"; + noNotificationsMessage.innerHTML = ` +

Ping? Pong! No notifications, moving along

+ `; + notificationPanel.appendChild(noNotificationsMessage); + } else { + data.forEach((notification) => { + const notificationItem = document.createElement("div"); + notificationItem.className = `notification-panel-item d-flex align-items-start p-3 ${ + notification.is_read ? "" : "notification-panel-unread" + }`; + notificationItem.innerHTML = `
${ @@ -39,11 +48,12 @@ function updateNotifications() { ).toLocaleString()}
`; - notificationItem.addEventListener("click", () => - notificationAction(notification.id) - ); - notificationPanel.appendChild(notificationItem); - }); + notificationItem.addEventListener("click", () => + notificationAction(notification.id) + ); + notificationPanel.appendChild(notificationItem); + }); + } updateUnreadCount(); }); From 597748b7a7711c395f369c4e100c0199370d4361 Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Fri, 30 Aug 2024 07:40:48 +0530 Subject: [PATCH 10/17] Introduce project and system wide notification --- web/api/serializers.py | 11 ++++-- web/api/urls.py | 2 +- web/api/views.py | 39 +++++++++++++++---- web/dashboard/admin.py | 1 + .../migrations/0003_auto_20240830_0135.py | 24 ++++++++++++ ...4_rename_notification_inappnotification.py | 17 ++++++++ web/dashboard/models.py | 20 ++++++++-- web/static/custom/notification.js | 25 +++++++++++- 8 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 web/dashboard/migrations/0003_auto_20240830_0135.py create mode 100644 web/dashboard/migrations/0004_rename_notification_inappnotification.py diff --git a/web/api/serializers.py b/web/api/serializers.py index 08dc1899c..be51d08e1 100644 --- a/web/api/serializers.py +++ b/web/api/serializers.py @@ -7,15 +7,18 @@ from scanEngine.models import * from startScan.models import * from targetApp.models import * -from dashboard.models import Notification +from dashboard.models import InAppNotification -class NotificationSerializer(serializers.ModelSerializer): +class InAppNotificationSerializer(serializers.ModelSerializer): class Meta: - model = Notification - fields = ['id', 'title', 'description', 'icon', 'is_read', 'created_at'] + model = InAppNotification + fields = ['id', 'title', 'description', 'icon', 'is_read', 'created_at', 'notification_type', 'project'] read_only_fields = ['id', 'created_at'] + def get_project_name(self, obj): + return obj.project.name if obj.project else None + class SearchHistorySerializer(serializers.ModelSerializer): class Meta: diff --git a/web/api/urls.py b/web/api/urls.py index 757a70b6a..39ceb84a7 100644 --- a/web/api/urls.py +++ b/web/api/urls.py @@ -19,7 +19,7 @@ router.register(r'listIps', IpAddressViewSet) router.register(r'listActivityLogs', ListActivityLogsViewSet) router.register(r'listScanLogs', ListScanLogsViewSet) -router.register(r'notifications', NotificationManagerViewSet, basename='notification') +router.register(r'notifications', InAppNotificationManagerViewSet, basename='notification') urlpatterns = [ url('^', include(router.urls)), diff --git a/web/api/views.py b/web/api/views.py index cf388fbcd..99d4d3aa0 100644 --- a/web/api/views.py +++ b/web/api/views.py @@ -34,23 +34,36 @@ logger = logging.getLogger(__name__) -class NotificationManagerViewSet(viewsets.ModelViewSet): +class InAppNotificationManagerViewSet(viewsets.ModelViewSet): """ This class manages the notification model, provided CRUD operation on notif model such as read notif, clear all, fetch all notifications etc """ - serializer_class = NotificationSerializer + serializer_class = InAppNotificationSerializer pagination_class = None def get_queryset(self): # we will see later if user based notif is needed - # return Notification.objects.filter(user=self.request.user) - return Notification.objects.all() + # return InAppNotification.objects.filter(user=self.request.user) + project_slug = self.request.query_params.get('project_slug') + queryset = InAppNotification.objects.all() + if project_slug: + queryset = queryset.filter( + Q(project__slug=project_slug) | Q(notification_type='system') + ) + return queryset.order_by('-created_at') @action(detail=False, methods=['post']) def mark_all_read(self, request): # marks all notification read - self.get_queryset().update(is_read=True) + project_slug = self.request.query_params.get('project_slug') + queryset = self.get_queryset() + + if project_slug: + queryset = queryset.filter( + Q(project__slug=project_slug) | Q(notification_type='system') + ) + queryset.update(is_read=True) return Response(status=HTTP_204_NO_CONTENT) @action(detail=True, methods=['post']) @@ -64,13 +77,25 @@ def mark_read(self, request, pk=None): @action(detail=False, methods=['get']) def unread_count(self, request): # this fetches the count for unread notif mainly for the badge - count = self.get_queryset().filter(is_read=False).count() + project_slug = self.request.query_params.get('project_slug') + queryset = self.get_queryset() + if project_slug: + queryset = queryset.filter( + Q(project__slug=project_slug) | Q(notification_type='system') + ) + count = queryset.filter(is_read=False).count() return Response({'count': count}) @action(detail=False, methods=['post']) def clear_all(self, request): # when clicked on the clear button this must be called to clear all notif - self.get_queryset().delete() + project_slug = self.request.query_params.get('project_slug') + queryset = self.get_queryset() + if project_slug: + queryset = queryset.filter( + Q(project__slug=project_slug) | Q(notification_type='system') + ) + queryset.delete() return Response(status=HTTP_204_NO_CONTENT) diff --git a/web/dashboard/admin.py b/web/dashboard/admin.py index be2a79a67..881b18acb 100644 --- a/web/dashboard/admin.py +++ b/web/dashboard/admin.py @@ -5,3 +5,4 @@ admin.site.register(Project) admin.site.register(OpenAiAPIKey) admin.site.register(NetlasAPIKey) +admin.site.register(InAppNotification) \ No newline at end of file diff --git a/web/dashboard/migrations/0003_auto_20240830_0135.py b/web/dashboard/migrations/0003_auto_20240830_0135.py new file mode 100644 index 000000000..5e7628e00 --- /dev/null +++ b/web/dashboard/migrations/0003_auto_20240830_0135.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.23 on 2024-08-30 01:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0002_notification'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='notification_type', + field=models.CharField(choices=[('system', 'System-wide'), ('project', 'Project-specific')], default='system', max_length=10), + ), + migrations.AddField( + model_name='notification', + name='project', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dashboard.project'), + ), + ] diff --git a/web/dashboard/migrations/0004_rename_notification_inappnotification.py b/web/dashboard/migrations/0004_rename_notification_inappnotification.py new file mode 100644 index 000000000..2bc145eed --- /dev/null +++ b/web/dashboard/migrations/0004_rename_notification_inappnotification.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.23 on 2024-08-30 01:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0003_auto_20240830_0135'), + ] + + operations = [ + migrations.RenameModel( + old_name='Notification', + new_name='InAppNotification', + ), + ] diff --git a/web/dashboard/models.py b/web/dashboard/models.py index b5e218579..0b491a010 100644 --- a/web/dashboard/models.py +++ b/web/dashboard/models.py @@ -43,10 +43,16 @@ def __str__(self): return self.key -class Notification(models.Model): +class InAppNotification(models.Model): + NOTIFICATION_TYPES = ( + ('system', 'System-wide'), + ('project', 'Project-specific'), + ) + project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True) + notification_type = models.CharField(max_length=10, choices=NOTIFICATION_TYPES, default='system') title = models.CharField(max_length=255) description = models.TextField() - icon = models.CharField(max_length=50) + icon = models.CharField(max_length=50) # mdi icon class name is_read = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) @@ -54,4 +60,12 @@ class Meta: ordering = ['-created_at'] def __str__(self): - return f"Notif {self.title}" + if self.notification_type == 'system': + return f"System wide notif: {self.title}" + else: + return f"Project wide notif: {self.project.name}: {self.title}" + + @property + def is_system_wide(self): + # property to determine if the notification is system wide or project specific + return self.notification_type == 'system' diff --git a/web/static/custom/notification.js b/web/static/custom/notification.js index 32470d8f8..c3f763505 100644 --- a/web/static/custom/notification.js +++ b/web/static/custom/notification.js @@ -2,7 +2,12 @@ // all the functions and event listeners for the notification panel function updateNotifications() { - fetch("/api/notifications/", { + let api_url = "/api/notifications/"; + const currentProjectSlug = getCurrentProjectSlug(); + if (currentProjectSlug) { + api_url += `?project_slug=${currentProjectSlug}`; + } + fetch(api_url, { method: "GET", credentials: "same-origin", headers: { @@ -60,7 +65,12 @@ function updateNotifications() { } function updateUnreadCount() { - fetch("/api/notifications/unread_count/", { + let api_url = "/api/notifications/unread_count/"; + const currentProjectSlug = getCurrentProjectSlug(); + if (currentProjectSlug) { + api_url += `?project_slug=${currentProjectSlug}`; + } + fetch(api_url, { method: "GET", credentials: "same-origin", headers: { @@ -92,6 +102,11 @@ function notificationAction(notificationId) { } function clearAllNotifications() { + let api_url = "/api/notifications/clear_all/"; + const currentProjectSlug = getCurrentProjectSlug(); + if (currentProjectSlug) { + api_url += `?project_slug=${currentProjectSlug}`; + } fetch("/api/notifications/clear_all/", { method: "POST", credentials: "same-origin", @@ -112,3 +127,9 @@ document.addEventListener("DOMContentLoaded", () => { updateNotifications(); setInterval(updateNotifications, 30000); }); + + +function getCurrentProjectSlug() { + const hiddenInput = document.querySelector('input[name="current_project"]'); + return hiddenInput ? hiddenInput.value : null; +} \ No newline at end of file From f18e55e8f14838af40dab56b524c99d7dc43bd93 Mon Sep 17 00:00:00 2001 From: Yogesh Ojha Date: Sat, 31 Aug 2024 08:35:36 +0530 Subject: [PATCH 11/17] Added notification status for priotiy --- web/api/serializers.py | 2 +- ...ter_inappnotification_notification_type.py | 18 +++++ .../0006_inappnotification_status.py | 18 +++++ web/dashboard/models.py | 7 +- web/reNgine/definitions.py | 15 ++++ web/static/custom/custom.css | 78 ++++++++++++++++--- web/static/custom/notification.js | 53 +++++++++++-- web/templates/base/_items/top_bar.html | 11 +-- web/templates/base/base.html | 1 + 9 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 web/dashboard/migrations/0005_alter_inappnotification_notification_type.py create mode 100644 web/dashboard/migrations/0006_inappnotification_status.py diff --git a/web/api/serializers.py b/web/api/serializers.py index be51d08e1..e355cd9fc 100644 --- a/web/api/serializers.py +++ b/web/api/serializers.py @@ -13,7 +13,7 @@ class InAppNotificationSerializer(serializers.ModelSerializer): class Meta: model = InAppNotification - fields = ['id', 'title', 'description', 'icon', 'is_read', 'created_at', 'notification_type', 'project'] + fields = ['id', 'title', 'description', 'icon', 'is_read', 'created_at', 'notification_type', 'status', 'project'] read_only_fields = ['id', 'created_at'] def get_project_name(self, obj): diff --git a/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py b/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py new file mode 100644 index 000000000..49a5af804 --- /dev/null +++ b/web/dashboard/migrations/0005_alter_inappnotification_notification_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-08-31 01:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0004_rename_notification_inappnotification'), + ] + + operations = [ + migrations.AlterField( + model_name='inappnotification', + name='notification_type', + field=models.CharField(choices=[('system', 'system'), ('project', 'project')], default='system', max_length=10), + ), + ] diff --git a/web/dashboard/migrations/0006_inappnotification_status.py b/web/dashboard/migrations/0006_inappnotification_status.py new file mode 100644 index 000000000..c674b7844 --- /dev/null +++ b/web/dashboard/migrations/0006_inappnotification_status.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-08-31 02:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dashboard', '0005_alter_inappnotification_notification_type'), + ] + + operations = [ + migrations.AddField( + model_name='inappnotification', + name='status', + field=models.CharField(choices=[('success', 'Success'), ('info', 'Informational'), ('warning', 'Warning'), ('error', 'Error')], default='info', max_length=10), + ), + ] diff --git a/web/dashboard/models.py b/web/dashboard/models.py index 0b491a010..4d764f08a 100644 --- a/web/dashboard/models.py +++ b/web/dashboard/models.py @@ -1,5 +1,5 @@ from django.db import models - +from reNgine.definitions import * class SearchHistory(models.Model): query = models.CharField(max_length=1000) @@ -44,12 +44,9 @@ def __str__(self): class InAppNotification(models.Model): - NOTIFICATION_TYPES = ( - ('system', 'System-wide'), - ('project', 'Project-specific'), - ) project = models.ForeignKey(Project, on_delete=models.CASCADE, null=True, blank=True) notification_type = models.CharField(max_length=10, choices=NOTIFICATION_TYPES, default='system') + status = models.CharField(max_length=10, choices=NOTIFICATION_STATUS_TYPES, default='info') title = models.CharField(max_length=255) description = models.TextField() icon = models.CharField(max_length=50) # mdi icon class name diff --git a/web/reNgine/definitions.py b/web/reNgine/definitions.py index e90ece4f5..58fce2dd5 100644 --- a/web/reNgine/definitions.py +++ b/web/reNgine/definitions.py @@ -548,3 +548,18 @@ # OSINT GooFuzz Path GOFUZZ_EXEC_PATH = '/usr/src/github/goofuzz/GooFuzz' + + +# In App Notification Definitions +SYSTEM_LEVEL_NOTIFICATION = 'system' +PROJECT_LEVEL_NOTIFICATION = 'project' +NOTIFICATION_TYPES = ( + ('system', SYSTEM_LEVEL_NOTIFICATION), + ('project', PROJECT_LEVEL_NOTIFICATION), +) +NOTIFICATION_STATUS_TYPES = ( + ('success', 'Success'), + ('info', 'Informational'), + ('warning', 'Warning'), + ('error', 'Error'), +) \ No newline at end of file diff --git a/web/static/custom/custom.css b/web/static/custom/custom.css index 227243885..47fee0e0b 100644 --- a/web/static/custom/custom.css +++ b/web/static/custom/custom.css @@ -422,7 +422,7 @@ mark{ } .notification-panel-dropdown { - width: 380px; + width: 400px; border: none; border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); @@ -452,8 +452,9 @@ mark{ } .notification-panel-item { - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - transition: background-color 0.2s ease; + border-left: 4px solid transparent; + transition: all 0.3s ease; + margin-bottom: 2px; } .notification-panel-item:last-child { @@ -461,38 +462,77 @@ mark{ } .notification-panel-item:hover { - background-color: #f8f9fa; + transform: translateY(-2px); + box-shadow: 0 2px 0px rgba(0, 0, 0, 0.10); +} + +/* status */ +.notification-panel-status-success { + border-left-color: #10B981; +} + +.notification-panel-status-info { + border-left-color: #3B82F6; +} + +.notification-panel-status-warning { + border-left-color: #F59E0B; +} + +.notification-panel-status-error { + border-left-color: #EF4444; } .notification-panel-unread { - background-color: #fafbff; - border-left: 3px solid #4a90e2; + background-color: #F3F4F6; } .notification-panel-unread:hover { - background-color: #f0f4ff; + background-color: #E5E7EB; } .notification-panel-icon { font-size: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #E5E7EB; } .notification-panel-title { - color: #212529; font-size: 0.95rem; font-weight: 600; } +.notification-panel-status-success .notification-panel-title { + color: #10B981; +} + +.notification-panel-status-info .notification-panel-title { + color: #3B82F6; +} + +.notification-panel-status-warning .notification-panel-title { + color: #F59E0B; +} + +.notification-panel-status-error .notification-panel-title { + color: #EF4444; +} + .notification-panel-description { - color: #6c757d; + color: #4B5563; font-size: 0.875rem; margin-bottom: 0.25rem; line-height: 1.4; } .notification-panel-time { - color: #adb5bd; - font-size: 0.75rem; + color: #6B7280; + font-size: 0.8rem; } .notification-panel-footer { @@ -512,6 +552,22 @@ mark{ color: #0056b3; } +.notification-panel-status-success .notification-panel-icon { + color: #10B981; +} + +.notification-panel-status-info .notification-panel-icon { + color: #3B82F6; +} + +.notification-panel-status-warning .notification-panel-icon { + color: #F59E0B; +} + +.notification-panel-status-error .notification-panel-icon { + color: #EF4444; +} + @keyframes notification-buzz { 0% { transform: translateX(0) rotate(0); } 15% { transform: translateX(-5px) rotate(-5deg); } diff --git a/web/static/custom/notification.js b/web/static/custom/notification.js index c3f763505..bd019c84d 100644 --- a/web/static/custom/notification.js +++ b/web/static/custom/notification.js @@ -1,6 +1,8 @@ // notifications.js // all the functions and event listeners for the notification panel +// notifications.js + function updateNotifications() { let api_url = "/api/notifications/"; const currentProjectSlug = getCurrentProjectSlug(); @@ -34,7 +36,7 @@ function updateNotifications() { const notificationItem = document.createElement("div"); notificationItem.className = `notification-panel-item d-flex align-items-start p-3 ${ notification.is_read ? "" : "notification-panel-unread" - }`; + } notification-panel-status-${notification.status}`; notificationItem.innerHTML = `
@@ -48,9 +50,9 @@ function updateNotifications() {

${ notification.description }

- ${new Date( - notification.created_at - ).toLocaleString()} + ${timeago.format( + new Date(notification.created_at) + )}
`; notificationItem.addEventListener("click", () => @@ -123,13 +125,52 @@ document.addEventListener("DOMContentLoaded", () => { const clearAllLink = document.querySelector("#clear-notif-btn"); clearAllLink.addEventListener("click", clearAllNotifications); + const markAllReadBtn = document.querySelector("#mark-all-read-btn"); + markAllReadBtn.addEventListener("click", markAllAsRead); + // Update notifications every 30 seconds updateNotifications(); setInterval(updateNotifications, 30000); -}); + setInterval(updateTimes, 60000); +}); function getCurrentProjectSlug() { const hiddenInput = document.querySelector('input[name="current_project"]'); return hiddenInput ? hiddenInput.value : null; -} \ No newline at end of file +} + +function markAllAsRead() { + fetch("/api/notifications/mark_all_read/", { + method: "POST", + credentials: "same-origin", + headers: { + "X-CSRFToken": getCookie("csrftoken"), + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.ok) { + document + .querySelectorAll(".notification-panel-item") + .forEach((item) => { + item.classList.remove("notification-panel-unread"); + }); + updateUnreadCount(); + } + }) + .catch((error) => + console.error("Error marking all notifications as read:", error) + ); +} + +function updateTimes() { + document + .querySelectorAll(".notification-panel-time") + .forEach((timeElement) => { + const datetime = timeElement.getAttribute("datetime"); + if (datetime) { + timeElement.textContent = timeago.format(new Date(datetime)); + } + }); +} diff --git a/web/templates/base/_items/top_bar.html b/web/templates/base/_items/top_bar.html index 114773b53..e2fb1ab7b 100644 --- a/web/templates/base/_items/top_bar.html +++ b/web/templates/base/_items/top_bar.html @@ -170,14 +170,15 @@
Welcome {{user.get_username}}!
diff --git a/web/templates/base/base.html b/web/templates/base/base.html index a8eefabb8..0fb2022cf 100644 --- a/web/templates/base/base.html +++ b/web/templates/base/base.html @@ -108,6 +108,7 @@

{% block page_title %}{% endblock page_title %}

+