Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app/common/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@

<span class="grow"></span>

@if (currentUser.role === 'Student') {
<f-notifications-button></f-notifications-button>
}

@if (currentUser.role === 'Admin' || currentUser.role === 'Convenor') {
<button
#menuState="matMenuTrigger"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.notification-panel {
position: absolute;
top: 60px;
right: 20px;
width: 400px;
max-height: 70vh;
background: white;
border: 1px solid #ccc;
border-radius: 6px;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
z-index: 1000;
overflow-y: auto;
padding: 20px 16px 16px;
scrollbar-width: thin;
scrollbar-color: #ccc transparent;
}

.notification-panel::-webkit-scrollbar {
width: 6px;
}

.notification-panel::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 10px;
}

.notification-content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 8px;
}

.notification-content span {
flex: 1;
margin-right: 8px;
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}

.delete-all-container {
display: flex;
justify-content: center;
margin-top: 4px;
}

.notification-button {
position: relative;
}

.notification-panel mat-list-item {
border-bottom: 1px solid #e0e0e0;
padding-bottom: 8px;
margin-bottom: 8px;
}

.notification-badge {
position: absolute;
top: 4px;
right: 4px;
background: red;
color: white;
font-size: 10px;
font-weight: bold;
border-radius: 50%;
padding: 2px 6px;
line-height: 1;
min-width: 16px;
text-align: center;
}






Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<button mat-icon-button (click)="toggleNotifications()" aria-label="Notifications" class="notification-button">
<mat-icon>notifications</mat-icon>
<span *ngIf="notifications.length > 0" class="notification-badge">{{ notifications.length }}</span>
</button>

<!-- Notification List Panel -->
<div *ngIf="showNotifications" class="notification-panel" #panel>
<mat-list>
<mat-list-item *ngFor="let note of notifications">
<div class="notification-content">
<span class="mat-body-2">{{ note.message }}</span>
<button mat-icon-button aria-label="Dismiss notification" (click)="dismissNotification(note.id)">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-list-item>

<!-- If no notifications -->
<mat-list-item *ngIf="notifications.length === 0">
<span class="mat-body-2">No notifications</span>
</mat-list-item>
</mat-list>

<div *ngIf="notifications.length > 0" class="delete-all-container">
<button mat-button color="warn" (click)="deleteAllNotifications()">Delete All</button>
</div>

<div class="close-container">
<button mat-button (click)="toggleNotifications()">Close</button>
</div>
</div>




Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { NotificationsButtonComponent } from './notifications-button.component';

describe('NotificationsButtonComponent', () => {
let component: NotificationsButtonComponent;
let fixture: ComponentFixture<NotificationsButtonComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NotificationsButtonComponent]
})
.compileComponents();

fixture = TestBed.createComponent(NotificationsButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Component, OnInit, OnDestroy, ElementRef, HostListener, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AppInjector } from 'src/app/app-injector';
import { DoubtfireConstants } from 'src/app/config/constants/doubtfire-constants';
import { Subscription } from 'rxjs';

// Define structure of a Notification object
interface Notification {
id: number;
message: string;
}

@Component({
selector: 'f-notifications-button',
templateUrl: './notifications-button.component.html',
styleUrls: ['./notifications-button.component.css']
})
export class NotificationsButtonComponent implements OnInit, OnDestroy {

// Tracks whether the notifications dropdown is visible
showNotifications = false;

// List of notifications to be displayed
notifications: Notification[] = [];

// Track active subscriptions for cleanup
private subscriptions = new Subscription();

// Reference to the notification panel in the DOM
@ViewChild('panel') panelRef!: ElementRef;

private readonly API_URL = AppInjector.get(DoubtfireConstants).API_URL;

constructor(private http: HttpClient) {}

ngOnInit() {
this.loadNotifications();
}

ngOnDestroy() {
// Prevent memory leaks
this.subscriptions.unsubscribe();
}

// Close panel when clicking outside of it
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent) {
if (this.showNotifications && this.panelRef && !this.panelRef.nativeElement.contains(event.target)) {
this.showNotifications = false;
}
}

// Toggle the visibility of the notifications dropdown
toggleNotifications() {
this.showNotifications = !this.showNotifications;
}

// Fetch notifications from the API
loadNotifications() {
this.subscriptions.add(
this.http.get<Notification[]>(`${this.API_URL}/notifications`).subscribe({
next: data => this.notifications = data,
error: err => console.error('Error loading notifications', err)
})
);
}

// Remove a specific notification by ID
dismissNotification(notificationId: number) {
this.subscriptions.add(
this.http.delete(`${this.API_URL}/notifications/${notificationId}`).subscribe({
next: () => {
this.notifications = this.notifications.filter(note => note.id !== notificationId);
},
error: err => console.error('Error deleting notification', err)
})
);
}

// Delete all notifications
deleteAllNotifications() {
this.subscriptions.add(
this.http.delete(`${this.API_URL}/notifications`).subscribe({
next: () => {
this.notifications = [];
},
error: err => console.error('Error deleting all notifications', err)
})
);
}

}

2 changes: 2 additions & 0 deletions src/app/doubtfire-angular.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ import {ScormExtensionModalComponent} from './common/modals/scorm-extension-moda
import { GradeIconComponent } from './common/grade-icon/grade-icon.component';
import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component';
import { PrivacyPolicy } from './config/privacy-policy/privacy-policy';
import {NotificationsButtonComponent} from './common/header/notifications-button/notifications-button.component';

// See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036
const MY_DATE_FORMAT = {
Expand Down Expand Up @@ -389,6 +390,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student-
TaskScormCardComponent,
ScormExtensionCommentComponent,
ScormExtensionModalComponent,
NotificationsButtonComponent,
],
// Services we provide
providers: [
Expand Down